diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index 3d7bfb78f..6fd164f2a 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -1,234 +1,256 @@ getDiffusionRequest(); $forms = array(); $form = id(new AphrontFormView()) ->setUser($this->getRequest()->getUser()) ->setMethod('GET'); switch ($drequest->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $forms[] = id(clone $form) ->appendChild(pht('Search is not available in Subversion.')); break; default: $forms[] = id(clone $form) ->appendChild( id(new AphrontFormTextWithSubmitControl()) ->setLabel(pht('File Name')) ->setSubmitLabel(pht('Search File Names')) ->setName('find') ->setValue($this->getRequest()->getStr('find'))); $forms[] = id(clone $form) ->appendChild( id(new AphrontFormTextWithSubmitControl()) ->setLabel(pht('Pattern')) ->setSubmitLabel(pht('Grep File Content')) ->setName('grep') ->setValue($this->getRequest()->getStr('grep'))); break; } $filter = new AphrontListFilterView(); $filter->appendChild($forms); if ($collapsed) { $filter->setCollapsed( pht('Show Search'), pht('Hide Search'), pht('Search for file names or content in this directory.'), '#'); } $filter = id(new PHUIBoxView()) ->addClass('mlt mlb') ->appendChild($filter); return $filter; } protected function markupText($text) { $engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine(); $engine->setConfig('viewer', $this->getRequest()->getUser()); $text = $engine->markupText($text); $text = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $text); return $text; } protected function buildHeaderView(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($this->renderPathLinks($drequest, $mode = 'browse')) ->setPolicyObject($drequest->getRepository()); return $header; } protected function buildActionView(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $view = id(new PhabricatorActionListView()) ->setUser($viewer); $history_uri = $drequest->generateURI( array( 'action' => 'history', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($history_uri) ->setIcon('fa-list')); $behind_head = $drequest->getSymbolicCommit(); $head_uri = $drequest->generateURI( array( 'commit' => '', 'action' => 'browse', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Jump to HEAD')) ->setHref($head_uri) ->setIcon('fa-home') ->setDisabled(!$behind_head)); - // TODO: Ideally, this should live in Owners and be event-triggered, but - // there's no reasonable object for it to react to right now. - - $owners = 'PhabricatorOwnersApplication'; - if (PhabricatorApplication::isClassInstalled($owners)) { - $owners_uri = id(new PhutilURI('/owners/view/search/')) - ->setQueryParams( - array( - 'repository' => $drequest->getCallsign(), - 'path' => '/'.$drequest->getPath(), - )); - - $view->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Find Owners')) - ->setHref((string)$owners_uri) - ->setIcon('fa-users')); - } - return $view; } protected function buildPropertyView( DiffusionRequest $drequest, PhabricatorActionListView $actions) { - $viewer = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setActionList($actions); $stable_commit = $drequest->getStableCommit(); $callsign = $drequest->getRepository()->getCallsign(); $view->addProperty( pht('Commit'), phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $stable_commit, )), ), $drequest->getRepository()->formatCommitName($stable_commit))); if ($drequest->getSymbolicType() == 'tag') { $symbolic = $drequest->getSymbolicCommit(); $view->addProperty(pht('Tag'), $symbolic); $tags = $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', array( 'names' => array($symbolic), 'needMessages' => true, )); $tags = DiffusionRepositoryTag::newFromConduit($tags); $tags = mpull($tags, null, 'getName'); $tag = idx($tags, $symbolic); if ($tag && strlen($tag->getMessage())) { $view->addSectionHeader(pht('Tag Content')); $view->addTextContent($this->markupText($tag->getMessage())); } } + $repository = $drequest->getRepository(); + + $owners = 'PhabricatorOwnersApplication'; + if (PhabricatorApplication::isClassInstalled($owners)) { + $package_query = id(new PhabricatorOwnersPackageQuery()) + ->setViewer($viewer) + ->withControl( + $repository->getPHID(), + array( + $drequest->getPath(), + )); + + $package_query->execute(); + + $packages = $package_query->getControllingPackagesForPath( + $repository->getPHID(), + $drequest->getPath()); + + if ($packages) { + $ownership = id(new PHUIStatusListView()) + ->setUser($viewer); + + + + foreach ($packages as $package) { + $icon = 'fa-list-alt'; + $color = 'grey'; + + $item = id(new PHUIStatusItemView()) + ->setIcon($icon, $color) + ->setTarget($viewer->renderHandle($package->getPHID())); + + $ownership->addItem($item); + } + } else { + $ownership = phutil_tag('em', array(), pht('None')); + } + + $view->addProperty(pht('Packages'), $ownership); + } + return $view; } protected function buildOpenRevisions() { $user = $this->getRequest()->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs(); $path_id = idx($path_map, $path); if (!$path_id) { return null; } $recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds')); $revisions = id(new DifferentialRevisionQuery()) ->setViewer($user) ->withPath($repository->getID(), $path_id) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->withUpdatedEpochBetween($recent, null) ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) ->setLimit(10) ->needRelationships(true) ->needFlags(true) ->needDrafts(true) ->execute(); if (!$revisions) { return null; } $header = id(new PHUIHeaderView()) ->setHeader(pht('Open Revisions')) ->setSubheader( pht('Recently updated open revisions affecting this file.')); $view = id(new DifferentialRevisionListView()) ->setHeader($header) ->setRevisions($revisions) ->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return $view; } } diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php index 6ec31eadf..c3f0ec42f 100644 --- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php +++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php @@ -1,288 +1,289 @@ getViewer(); $package = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) + ->needPaths(true) ->executeOne(); if (!$package) { return new Aphront404Response(); } - $paths = $package->loadPaths(); + $paths = $package->getPaths(); $repository_phids = array(); foreach ($paths as $path) { $repository_phids[$path->getRepositoryPHID()] = true; } if ($repository_phids) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($repository_phids)) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); } else { $repositories = array(); } $actions = $this->buildPackageActionView($package); $properties = $this->buildPackagePropertyView($package); $properties->setActionList($actions); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($package->getName()) ->setPolicyObject($package); $panel = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $commit_views = array(); $commit_uri = id(new PhutilURI('/audit/')) ->setQueryParams( array( 'auditorPHIDs' => $package->getPHID(), )); $attention_commits = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withAuditorPHIDs(array($package->getPHID())) ->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_CONCERN) ->needCommitData(true) ->setLimit(10) ->execute(); if ($attention_commits) { $view = id(new PhabricatorAuditListView()) ->setUser($viewer) ->setCommits($attention_commits); $commit_views[] = array( 'view' => $view, 'header' => pht('Commits in this Package that Need Attention'), 'button' => id(new PHUIButtonView()) ->setTag('a') ->setHref($commit_uri->alter('status', 'open')) ->setText(pht('View All Problem Commits')), ); } $all_commits = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withAuditorPHIDs(array($package->getPHID())) ->needCommitData(true) ->setLimit(100) ->execute(); $view = id(new PhabricatorAuditListView()) ->setUser($viewer) ->setCommits($all_commits) ->setNoDataString(pht('No commits in this package.')); $commit_views[] = array( 'view' => $view, 'header' => pht('Recent Commits in Package'), 'button' => id(new PHUIButtonView()) ->setTag('a') ->setHref($commit_uri) ->setText(pht('View All Package Commits')), ); $phids = array(); foreach ($commit_views as $commit_view) { $phids[] = $commit_view['view']->getRequiredHandlePHIDs(); } $phids = array_mergev($phids); $handles = $this->loadViewerHandles($phids); $commit_panels = array(); foreach ($commit_views as $commit_view) { $commit_panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $header->setHeader($commit_view['header']); if (isset($commit_view['button'])) { $header->addActionLink($commit_view['button']); } $commit_view['view']->setHandles($handles); $commit_panel->setHeader($header); $commit_panel->appendChild($commit_view['view']); $commit_panels[] = $commit_panel; } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($package->getName()); $timeline = $this->buildTransactionTimeline( $package, new PhabricatorOwnersPackageTransactionQuery()); $timeline->setShouldTerminate(true); return $this->buildApplicationPage( array( $crumbs, $panel, $this->renderPathsTable($paths, $repositories), $commit_panels, $timeline, ), array( 'title' => $package->getName(), )); } private function buildPackagePropertyView(PhabricatorOwnersPackage $package) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $primary_phid = $package->getPrimaryOwnerPHID(); if ($primary_phid) { $primary_owner = $viewer->renderHandle($primary_phid); } else { $primary_owner = phutil_tag('em', array(), pht('None')); } $view->addProperty(pht('Primary Owner'), $primary_owner); // TODO: needOwners() this on the Query. $owners = $package->loadOwners(); if ($owners) { $owner_list = $viewer->renderHandleList(mpull($owners, 'getUserPHID')); } else { $owner_list = phutil_tag('em', array(), pht('None')); } $view->addProperty(pht('Owners'), $owner_list); if ($package->getAuditingEnabled()) { $auditing = pht('Enabled'); } else { $auditing = pht('Disabled'); } $view->addProperty(pht('Auditing'), $auditing); $description = $package->getDescription(); if (strlen($description)) { $view->addSectionHeader(pht('Description')); $view->addTextContent( $output = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($description), 'default', $viewer)); } return $view; } private function buildPackageActionView(PhabricatorOwnersPackage $package) { $viewer = $this->getViewer(); // TODO: Implement this capability. $can_edit = true; $id = $package->getID(); $edit_uri = $this->getApplicationURI("/edit/{$id}/"); $paths_uri = $this->getApplicationURI("/paths/{$id}/"); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($package) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Package')) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($edit_uri)) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Paths')) ->setIcon('fa-folder-open') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($paths_uri)); return $view; } private function renderPathsTable(array $paths, array $repositories) { $viewer = $this->getViewer(); $rows = array(); foreach ($paths as $path) { $repo = idx($repositories, $path->getRepositoryPHID()); if (!$repo) { continue; } $href = DiffusionRequest::generateDiffusionURI( array( 'callsign' => $repo->getCallsign(), 'branch' => $repo->getDefaultBranch(), 'path' => $path->getPath(), 'action' => 'browse', )); $path_link = phutil_tag( 'a', array( 'href' => (string)$href, ), $path->getPath()); $rows[] = array( ($path->getExcluded() ? '-' : '+'), $repo->getName(), $path_link, ); } $info = null; if (!$paths) { $info = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors( array( pht( 'This package does not contain any paths yet. Use '. '"Edit Paths" to add some.'), )); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Repository'), pht('Path'), )) ->setColumnClasses( array( null, null, 'wide', )); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Paths')) ->setTable($table); if ($info) { $box->setInfoView($info); } return $box; } } diff --git a/src/applications/owners/controller/PhabricatorOwnersPathsController.php b/src/applications/owners/controller/PhabricatorOwnersPathsController.php index 95df2cb80..ad278d529 100644 --- a/src/applications/owners/controller/PhabricatorOwnersPathsController.php +++ b/src/applications/owners/controller/PhabricatorOwnersPathsController.php @@ -1,165 +1,166 @@ getUser(); $package = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, // TODO: Support this capability. // PhabricatorPolicyCapability::CAN_EDIT, )) + ->needPaths(true) ->executeOne(); if (!$package) { return new Aphront404Response(); } if ($request->isFormPost()) { $paths = $request->getArr('path'); $repos = $request->getArr('repo'); $excludes = $request->getArr('exclude'); $path_refs = array(); foreach ($paths as $key => $path) { if (!isset($repos[$key])) { throw new Exception( pht( 'No repository PHID for path "%s"!', $key)); } if (!isset($excludes[$key])) { throw new Exception( pht( 'No exclusion value for path "%s"!', $key)); } $path_refs[] = array( 'repositoryPHID' => $repos[$key], 'path' => $path, 'excluded' => (int)$excludes[$key], ); } $type_paths = PhabricatorOwnersPackageTransaction::TYPE_PATHS; $xactions = array(); $xactions[] = id(new PhabricatorOwnersPackageTransaction()) ->setTransactionType($type_paths) ->setNewValue($path_refs); $editor = id(new PhabricatorOwnersPackageTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $editor->applyTransactions($package, $xactions); return id(new AphrontRedirectResponse()) ->setURI('/owners/package/'.$package->getID().'/'); } else { - $paths = $package->loadPaths(); + $paths = $package->getPaths(); $path_refs = mpull($paths, 'getRef'); } $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->execute(); $default_paths = array(); foreach ($repos as $repo) { $default_path = $repo->getDetail('default-owners-path'); if ($default_path) { $default_paths[$repo->getPHID()] = $default_path; } } $repos = mpull($repos, 'getCallsign', 'getPHID'); asort($repos); $template = new AphrontTypeaheadTemplateView(); $template = $template->render(); Javelin::initBehavior( 'owners-path-editor', array( 'root' => 'path-editor', 'table' => 'paths', 'add_button' => 'addpath', 'repositories' => $repos, 'input_template' => $template, 'pathRefs' => $path_refs, 'completeURI' => '/diffusion/services/path/complete/', 'validateURI' => '/diffusion/services/path/validate/', 'repositoryDefaultPaths' => $default_paths, )); require_celerity_resource('owners-path-editor-css'); $cancel_uri = '/owners/package/'.$package->getID().'/'; $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new PHUIFormInsetView()) ->setTitle(pht('Paths')) ->addDivAttributes(array('id' => 'path-editor')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'addpath', 'mustcapture' => true, ), pht('Add New Path'))) ->setDescription( pht( 'Specify the files and directories which comprise '. 'this package.')) ->setContent(javelin_tag( 'table', array( 'class' => 'owners-path-editor-table', 'sigil' => 'paths', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue(pht('Save Paths'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Paths')) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $package->getName(), $this->getApplicationURI('package/'.$package->getID().'/')); $crumbs->addTextCrumb(pht('Edit Paths')); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => array( $package->getName(), pht('Edit Paths'), ), )); } } diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php index 8ce1e48c2..311cf642a 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php @@ -1,290 +1,288 @@ getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: return $object->getName(); case PhabricatorOwnersPackageTransaction::TYPE_PRIMARY: return $object->getPrimaryOwnerPHID(); case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: // TODO: needOwners() this on the Query. $phids = mpull($object->loadOwners(), 'getUserPHID'); $phids = array_values($phids); return $phids; case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: return (int)$object->getAuditingEnabled(); case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: return $object->getDescription(); case PhabricatorOwnersPackageTransaction::TYPE_PATHS: - // TODO: needPaths() this on the query - $paths = $object->loadPaths(); + $paths = $object->getPaths(); return mpull($paths, 'getRef'); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: case PhabricatorOwnersPackageTransaction::TYPE_PRIMARY: case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: case PhabricatorOwnersPackageTransaction::TYPE_PATHS: return $xaction->getNewValue(); case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: return (int)$xaction->getNewValue(); case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: $phids = $xaction->getNewValue(); $phids = array_unique($phids); $phids = array_values($phids); return $phids; } } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_PATHS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); list($rem, $add) = $diffs; return ($rem || $add); } return parent::transactionHasEffect($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; case PhabricatorOwnersPackageTransaction::TYPE_PRIMARY: $object->setPrimaryOwnerPHID($xaction->getNewValue()); return; case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: $object->setDescription($xaction->getNewValue()); return; case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: $object->setAuditingEnabled($xaction->getNewValue()); return; case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: case PhabricatorOwnersPackageTransaction::TYPE_PATHS: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: case PhabricatorOwnersPackageTransaction::TYPE_PRIMARY: case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: return; case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); // TODO: needOwners this $owners = $object->loadOwners(); $owners = mpull($owners, null, 'getUserPHID'); $rem = array_diff($old, $new); foreach ($rem as $phid) { if (isset($owners[$phid])) { $owners[$phid]->delete(); unset($owners[$phid]); } } $add = array_diff($new, $old); foreach ($add as $phid) { $owners[$phid] = id(new PhabricatorOwnersOwner()) ->setPackageID($object->getID()) ->setUserPHID($phid) ->save(); } // TODO: Attach owners here return; case PhabricatorOwnersPackageTransaction::TYPE_PATHS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); - // TODO: needPaths this - $paths = $object->loadPaths(); + $paths = $object->getPaths(); $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); list($rem, $add) = $diffs; $set = PhabricatorOwnersPath::getSetFromTransactionValue($rem); foreach ($paths as $path) { $ref = $path->getRef(); if (PhabricatorOwnersPath::isRefInSet($ref, $set)) { $path->delete(); } } foreach ($add as $ref) { $path = PhabricatorOwnersPath::newFromRef($ref) ->setPackageID($object->getID()) ->save(); } return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: $missing = $this->validateIsEmptyTextField( $object->getName(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Package name is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } break; case PhabricatorOwnersPackageTransaction::TYPE_PRIMARY: $missing = $this->validateIsEmptyTextField( $object->getPrimaryOwnerPHID(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Packages must have a primary owner.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } break; } return $errors; } protected function extractFilePHIDsFromCustomTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: return array($xaction->getNewValue()); } return parent::extractFilePHIDsFromCustomTransaction($object, $xaction); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.package.subject-prefix'); } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getPrimaryOwnerPHID(), $this->requireActor()->getPHID(), ); } protected function getMailCC(PhabricatorLiskDAO $object) { // TODO: needOwners() this return mpull($object->loadOwners(), 'getUserPHID'); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new OwnersPackageReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) ->setSubject($name) ->addHeader('Thread-Topic', $object->getPHID()); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $detail_uri = PhabricatorEnv::getProductionURI( '/owners/package/'.$object->getID().'/'); $body->addLinkSection( pht('PACKAGE DETAIL'), $detail_uri); return $body; } } diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php index 1d34d7be5..8dbb74e52 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php @@ -1,170 +1,317 @@ ownerPHIDs = $phids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withRepositoryPHIDs(array $phids) { $this->repositoryPHIDs = $phids; return $this; } + public function withControl($repository_phid, array $paths) { + if (empty($this->controlMap[$repository_phid])) { + $this->controlMap[$repository_phid] = array(); + } + + foreach ($paths as $path) { + $this->controlMap[$repository_phid][$path] = $path; + } + + // We need to load paths to execute control queries. + $this->needPaths = true; + + return $this; + } + public function withNamePrefix($prefix) { $this->namePrefix = $prefix; return $this; } + public function needPaths($need_paths) { + $this->needPaths = $need_paths; + return $this; + } + public function newResultObject() { return new PhabricatorOwnersPackage(); } + protected function willExecute() { + $this->controlResults = array(); + } + protected function loadPage() { return $this->loadStandardPage(new PhabricatorOwnersPackage()); } + protected function didFilterPage(array $packages) { + if ($this->needPaths) { + $package_ids = mpull($packages, 'getID'); + + $paths = id(new PhabricatorOwnersPath())->loadAllWhere( + 'packageID IN (%Ld)', + $package_ids); + $paths = mgroup($paths, 'getPackageID'); + + foreach ($packages as $package) { + $package->attachPaths(idx($paths, $package->getID(), array())); + } + } + + if ($this->controlMap) { + $this->controlResults += mpull($packages, null, 'getID'); + } + + return $packages; + } + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->ownerPHIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T o ON o.packageID = p.id', id(new PhabricatorOwnersOwner())->getTableName()); } - if ($this->repositoryPHIDs !== null) { + if ($this->shouldJoinOwnersPathTable()) { $joins[] = qsprintf( $conn, 'JOIN %T rpath ON rpath.packageID = p.id', id(new PhabricatorOwnersPath())->getTableName()); } return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->phids !== null) { $where[] = qsprintf( $conn, 'p.phid IN (%Ls)', $this->phids); } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'p.id IN (%Ld)', $this->ids); } if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( $conn, 'rpath.repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } if ($this->ownerPHIDs !== null) { $base_phids = $this->ownerPHIDs; $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($base_phids) ->execute(); $project_phids = mpull($projects, 'getPHID'); $all_phids = array_merge($base_phids, $project_phids); $where[] = qsprintf( $conn, 'o.userPHID IN (%Ls)', $all_phids); } if (strlen($this->namePrefix)) { // NOTE: This is a hacky mess, but this column is currently case // sensitive and unique. $where[] = qsprintf( $conn, 'LOWER(p.name) LIKE %>', phutil_utf8_strtolower($this->namePrefix)); } + if ($this->controlMap) { + $clauses = array(); + foreach ($this->controlMap as $repository_phid => $paths) { + $fragments = array(); + foreach ($paths as $path) { + foreach (PhabricatorOwnersPackage::splitPath($path) as $fragment) { + $fragments[$fragment] = $fragment; + } + } + + $clauses[] = qsprintf( + $conn, + '(rpath.repositoryPHID = %s AND rpath.path IN (%Ls))', + $repository_phid, + $fragments); + } + $where[] = implode(' OR ', $clauses); + } + return $where; } protected function shouldGroupQueryResultRows() { - if ($this->repositoryPHIDs) { + if ($this->shouldJoinOwnersPathTable()) { return true; } if ($this->ownerPHIDs) { return true; } return parent::shouldGroupQueryResultRows(); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Name'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'type' => 'string', 'unique' => true, 'reverse' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $package = $this->loadCursorObject($cursor); return array( 'id' => $package->getID(), 'name' => $package->getName(), ); } public function getQueryApplicationClass() { return 'PhabricatorOwnersApplication'; } protected function getPrimaryTableAlias() { return 'p'; } + private function shouldJoinOwnersPathTable() { + if ($this->repositoryPHIDs !== null) { + return true; + } + + if ($this->controlMap) { + return true; + } + + return false; + } + + +/* -( Path Control )------------------------------------------------------- */ + + + /** + * Get the package which controls a path, if one exists. + * + * @return PhabricatorOwnersPackage|null Package, if one exists. + */ + public function getControllingPackageForPath($repository_phid, $path) { + $packages = $this->getControllingPackagesForPath($repository_phid, $path); + + if (!$packages) { + return null; + } + + return head($packages); + } + + + /** + * Get a list of all packages which control a path or its parent directories, + * ordered from weakest to strongest. + * + * The first package has the most specific claim on the path; the last + * package has the most general claim. + * + * @return list List of controlling packages. + */ + public function getControllingPackagesForPath($repository_phid, $path) { + if (!isset($this->controlMap[$repository_phid][$path])) { + throw new PhutilInvalidStateException('withControl'); + } + + if ($this->controlResults === null) { + throw new PhutilInvalidStateException('execute'); + } + + $packages = $this->controlResults; + + $matches = array(); + foreach ($packages as $package_id => $package) { + $best_match = null; + $include = false; + + foreach ($package->getPaths() as $package_path) { + $strength = $package_path->getPathMatchStrength($path); + if ($strength > $best_match) { + $best_match = $strength; + $include = !$package_path->getExcluded(); + } + } + + if ($best_match && $include) { + $matches[$package_id] = array( + 'strength' => $best_match, + 'package' => $package, + ); + } + } + + $matches = isort($matches, 'strength'); + $matches = array_reverse($matches); + + return array_values(ipull($matches, 'package')); + } + } diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index ecf186232..82c8ca907 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -1,263 +1,275 @@ setAuditingEnabled(0) ->setPrimaryOwnerPHID($actor->getPHID()); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } protected function getConfiguration() { return array( // This information is better available from the history table. self::CONFIG_TIMESTAMPS => false, self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'originalName' => 'text255', 'description' => 'text', 'primaryOwnerPHID' => 'phid?', 'auditingEnabled' => 'bool', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorOwnersPackagePHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function setName($name) { $this->name = $name; if (!$this->getID()) { $this->originalName = $name; } return $this; } public function loadOwners() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID = %d', $this->getID()); } public function loadPaths() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID = %d', $this->getID()); } public static function loadAffectedPackages( PhabricatorRepository $repository, array $paths) { if (!$paths) { return array(); } return self::loadPackagesForPaths($repository, $paths); } public static function loadOwningPackages($repository, $path) { if (empty($path)) { return array(); } return self::loadPackagesForPaths($repository, array($path), 1); } private static function loadPackagesForPaths( PhabricatorRepository $repository, array $paths, $limit = 0) { $fragments = array(); foreach ($paths as $path) { foreach (self::splitPath($path) as $fragment) { $fragments[$fragment][$path] = true; } } $package = new PhabricatorOwnersPackage(); $path = new PhabricatorOwnersPath(); $conn = $package->establishConnection('r'); $repository_clause = qsprintf( $conn, 'AND p.repositoryPHID = %s', $repository->getPHID()); // NOTE: The list of $paths may be very large if we're coming from // the OwnersWorker and processing, e.g., an SVN commit which created a new // branch. Break it apart so that it will fit within 'max_allowed_packet', // and then merge results in PHP. $rows = array(); foreach (array_chunk(array_keys($fragments), 128) as $chunk) { $rows[] = queryfx_all( $conn, 'SELECT pkg.id, p.excluded, p.path FROM %T pkg JOIN %T p ON p.packageID = pkg.id WHERE p.path IN (%Ls) %Q', $package->getTableName(), $path->getTableName(), $chunk, $repository_clause); } $rows = array_mergev($rows); $ids = self::findLongestPathsPerPackage($rows, $fragments); if (!$ids) { return array(); } arsort($ids); if ($limit) { $ids = array_slice($ids, 0, $limit, $preserve_keys = true); } $ids = array_keys($ids); $packages = $package->loadAllWhere('id in (%Ld)', $ids); $packages = array_select_keys($packages, $ids); return $packages; } public static function loadPackagesForRepository($repository) { $package = new PhabricatorOwnersPackage(); $ids = ipull( queryfx_all( $package->establishConnection('r'), 'SELECT DISTINCT packageID FROM %T WHERE repositoryPHID = %s', id(new PhabricatorOwnersPath())->getTableName(), $repository->getPHID()), 'packageID'); return $package->loadAllWhere('id in (%Ld)', $ids); } public static function findLongestPathsPerPackage(array $rows, array $paths) { $ids = array(); foreach (igroup($rows, 'id') as $id => $package_paths) { $relevant_paths = array_select_keys( $paths, ipull($package_paths, 'path')); // For every package, remove all excluded paths. $remove = array(); foreach ($package_paths as $package_path) { if ($package_path['excluded']) { $remove += idx($relevant_paths, $package_path['path'], array()); unset($relevant_paths[$package_path['path']]); } } if ($remove) { foreach ($relevant_paths as $fragment => $fragment_paths) { $relevant_paths[$fragment] = array_diff_key($fragment_paths, $remove); } } $relevant_paths = array_filter($relevant_paths); if ($relevant_paths) { $ids[$id] = max(array_map('strlen', array_keys($relevant_paths))); } } return $ids; } - private static function splitPath($path) { + public static function splitPath($path) { $result = array('/'); $trailing_slash = preg_match('@/$@', $path) ? '/' : ''; $path = trim($path, '/'); $parts = explode('/', $path); while (count($parts)) { $result[] = '/'.implode('/', $parts).$trailing_slash; $trailing_slash = '/'; array_pop($parts); } return $result; } + public function attachPaths(array $paths) { + assert_instances_of($paths, 'PhabricatorOwnersPath'); + $this->paths = $paths; + return $this; + } + + public function getPaths() { + return $this->assertAttached($this->paths); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorOwnersPackageTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorOwnersPackageTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/owners/storage/PhabricatorOwnersPath.php b/src/applications/owners/storage/PhabricatorOwnersPath.php index f65d6052d..33ab10971 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPath.php +++ b/src/applications/owners/storage/PhabricatorOwnersPath.php @@ -1,73 +1,105 @@ false, self::CONFIG_COLUMN_SCHEMA => array( 'path' => 'text255', 'excluded' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'packageID' => array( 'columns' => array('packageID'), ), ), ) + parent::getConfiguration(); } public static function newFromRef(array $ref) { $path = new PhabricatorOwnersPath(); $path->repositoryPHID = $ref['repositoryPHID']; $path->path = $ref['path']; $path->excluded = $ref['excluded']; return $path; } public function getRef() { return array( 'repositoryPHID' => $this->getRepositoryPHID(), 'path' => $this->getPath(), 'excluded' => (int)$this->getExcluded(), ); } public static function getTransactionValueChanges(array $old, array $new) { return array( self::getTransactionValueDiff($old, $new), self::getTransactionValueDiff($new, $old), ); } private static function getTransactionValueDiff(array $u, array $v) { $set = self::getSetFromTransactionValue($v); foreach ($u as $key => $ref) { if (self::isRefInSet($ref, $set)) { unset($u[$key]); } } return $u; } public static function getSetFromTransactionValue(array $v) { $set = array(); foreach ($v as $ref) { $set[$ref['repositoryPHID']][$ref['path']][$ref['excluded']] = true; } return $set; } public static function isRefInSet(array $ref, array $set) { return isset($set[$ref['repositoryPHID']][$ref['path']][$ref['excluded']]); } + /** + * Get the number of directory matches between this path specification and + * some real path. + */ + public function getPathMatchStrength($path) { + $this_path = $this->getPath(); + + if ($this_path === '/') { + // The root path "/" just matches everything with strength 1. + return 1; + } + + $self_fragments = PhabricatorOwnersPackage::splitPath($this_path); + $path_fragments = PhabricatorOwnersPackage::splitPath($path); + + $self_count = count($self_fragments); + $path_count = count($path_fragments); + if ($self_count > $path_count) { + // If this path is longer (and therefor more specific) than the target + // path, we don't match it at all. + return 0; + } + + for ($ii = 0; $ii < $self_count; $ii++) { + if ($self_fragments[$ii] != $path_fragments[$ii]) { + return 0; + } + } + + return $self_count; + } + }