diff --git a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php index 9f4bd42f2..03476f70a 100644 --- a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php @@ -1,53 +1,53 @@ getPHID()] = true; $owned_packages = id(new PhabricatorOwnersPackageQuery()) ->setViewer($user) - ->withOwnerPHIDs(array($user->getPHID())) + ->withAuthorityPHIDs(array($user->getPHID())) ->execute(); foreach ($owned_packages as $package) { $phids[$package->getPHID()] = true; } // The user can audit on behalf of all projects they are a member of. $projects = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withMemberPHIDs(array($user->getPHID())) ->execute(); foreach ($projects as $project) { $phids[$project->getPHID()] = true; } return array_keys($phids); } public static function getMailThreading( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { return array( 'diffusion-audit-'.$commit->getPHID(), pht( 'Commit %s', 'r'.$repository->getCallsign().$commit->getCommitIdentifier()), ); } } diff --git a/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php b/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php index 6ce996a00..a3b7b52ca 100644 --- a/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php +++ b/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php @@ -1,161 +1,161 @@ 'optional string', 'projectOwner' => 'optional string', 'userAffiliated' => 'optional string', 'repositoryCallsign' => 'optional string', 'path' => 'optional string', ); } protected function defineReturnType() { return 'dict dict of package info>'; } protected function defineErrorTypes() { return array( 'ERR-INVALID-USAGE' => pht( 'Provide one of a single owner phid (user/project), a single '. 'affiliated user phid (user), or a repository/path.'), 'ERR-INVALID-PARAMETER' => pht('Parameter should be a phid.'), 'ERR_REP_NOT_FOUND' => pht('The repository callsign is not recognized.'), ); } protected static function queryAll() { return id(new PhabricatorOwnersPackage())->loadAll(); } protected static function queryByOwner($owner) { $is_valid_phid = phid_get_type($owner) == PhabricatorPeopleUserPHIDType::TYPECONST || phid_get_type($owner) == PhabricatorProjectProjectPHIDType::TYPECONST; if (!$is_valid_phid) { throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription( pht( 'Expected user/project PHID for owner, got %s.', $owner)); } $owners = id(new PhabricatorOwnersOwner())->loadAllWhere( 'userPHID = %s', $owner); $package_ids = mpull($owners, 'getPackageID'); $packages = array(); foreach ($package_ids as $id) { $packages[] = id(new PhabricatorOwnersPackage())->load($id); } return $packages; } private static function queryByPath( PhabricatorUser $viewer, $repo_callsign, $path) { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withCallsigns(array($repo_callsign)) ->executeOne(); if (!$repository) { throw id(new ConduitException('ERR_REP_NOT_FOUND')) ->setErrorDescription( pht( 'Repository callsign %s not recognized', $repo_callsign)); } if ($path == null) { return PhabricatorOwnersPackage::loadPackagesForRepository($repository); } else { return PhabricatorOwnersPackage::loadOwningPackages( $repository, $path); } } public static function buildPackageInformationDictionaries($packages) { assert_instances_of($packages, 'PhabricatorOwnersPackage'); $result = array(); foreach ($packages as $package) { $p_owners = $package->loadOwners(); $p_paths = $package->loadPaths(); $owners = array_values(mpull($p_owners, 'getUserPHID')); $paths = array(); foreach ($p_paths as $p) { $paths[] = array($p->getRepositoryPHID(), $p->getPath()); } $result[$package->getPHID()] = array( 'phid' => $package->getPHID(), 'name' => $package->getName(), 'description' => $package->getDescription(), 'owners' => $owners, 'paths' => $paths, ); } return $result; } protected function execute(ConduitAPIRequest $request) { $is_owner_query = ($request->getValue('userOwner') || $request->getValue('projectOwner')) ? 1 : 0; $is_affiliated_query = $request->getValue('userAffiliated') ? 1 : 0; $repo = $request->getValue('repositoryCallsign'); $path = $request->getValue('path'); $is_path_query = $repo ? 1 : 0; if ($is_owner_query + $is_path_query + $is_affiliated_query === 0) { // if no search terms are provided, return everything $packages = self::queryAll(); } else if ($is_owner_query + $is_path_query + $is_affiliated_query > 1) { // otherwise, exactly one of these should be provided throw new ConduitException('ERR-INVALID-USAGE'); } if ($is_affiliated_query) { $query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($request->getUser()); - $query->withOwnerPHIDs(array($request->getValue('userAffiliated'))); + $query->withAuthorityPHIDs(array($request->getValue('userAffiliated'))); $packages = $query->execute(); } else if ($is_owner_query) { $owner = nonempty( $request->getValue('userOwner'), $request->getValue('projectOwner')); $packages = self::queryByOwner($owner); } else if ($is_path_query) { $packages = self::queryByPath($request->getUser(), $repo, $path); } return self::buildPackageInformationDictionaries($packages); } } diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php index 168d7cd07..008cb715e 100644 --- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php +++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php @@ -1,281 +1,281 @@ getViewer(); $package = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->needPaths(true) + ->needOwners(true) ->executeOne(); if (!$package) { return new Aphront404Response(); } $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); - // TODO: needOwners() this on the Query. - $owners = $package->loadOwners(); + $owners = $package->getOwners(); 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/PhabricatorOwnersEditController.php b/src/applications/owners/controller/PhabricatorOwnersEditController.php index 618f99456..f601cd311 100644 --- a/src/applications/owners/controller/PhabricatorOwnersEditController.php +++ b/src/applications/owners/controller/PhabricatorOwnersEditController.php @@ -1,171 +1,171 @@ getUser(); $id = $request->getURIData('id'); if ($id) { $package = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, // TODO: Support this capability. // PhabricatorPolicyCapability::CAN_EDIT, )) + ->needOwners(true) ->executeOne(); if (!$package) { return new Aphront404Response(); } $is_new = false; } else { $package = PhabricatorOwnersPackage::initializeNewPackage($viewer); $is_new = true; } $e_name = true; $v_name = $package->getName(); - // TODO: Pull these off needOwners() on the Query. - $v_owners = mpull($package->loadOwners(), 'getUserPHID'); + $v_owners = mpull($package->getOwners(), 'getUserPHID'); $v_auditing = $package->getAuditingEnabled(); $v_description = $package->getDescription(); $errors = array(); if ($request->isFormPost()) { $xactions = array(); $v_name = $request->getStr('name'); $v_owners = $request->getArr('owners'); $v_auditing = ($request->getStr('auditing') == 'enabled'); $v_description = $request->getStr('description'); $type_name = PhabricatorOwnersPackageTransaction::TYPE_NAME; $type_owners = PhabricatorOwnersPackageTransaction::TYPE_OWNERS; $type_auditing = PhabricatorOwnersPackageTransaction::TYPE_AUDITING; $type_description = PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION; $xactions[] = id(new PhabricatorOwnersPackageTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(new PhabricatorOwnersPackageTransaction()) ->setTransactionType($type_owners) ->setNewValue($v_owners); $xactions[] = id(new PhabricatorOwnersPackageTransaction()) ->setTransactionType($type_auditing) ->setNewValue($v_auditing); $xactions[] = id(new PhabricatorOwnersPackageTransaction()) ->setTransactionType($type_description) ->setNewValue($v_description); $editor = id(new PhabricatorOwnersPackageTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($package, $xactions); $id = $package->getID(); if ($is_new) { $next_uri = '/owners/paths/'.$id.'/'; } else { $next_uri = '/owners/package/'.$id.'/'; } return id(new AphrontRedirectResponse())->setURI($next_uri); } catch (AphrontDuplicateKeyQueryException $ex) { $e_name = pht('Duplicate'); $errors[] = pht('Package name must be unique.'); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_name = $ex->getShortMessage($type_name); } } if ($is_new) { $cancel_uri = '/owners/'; $title = pht('New Package'); $button_text = pht('Continue'); } else { $cancel_uri = '/owners/package/'.$package->getID().'/'; $title = pht('Edit Package'); $button_text = pht('Save Package'); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($v_name) ->setError($e_name)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorProjectOrUserDatasource()) ->setLabel(pht('Owners')) ->setName('owners') ->setValue($v_owners)) ->appendChild( id(new AphrontFormSelectControl()) ->setName('auditing') ->setLabel(pht('Auditing')) ->setCaption( pht( 'With auditing enabled, all future commits that touch '. 'this package will be reviewed to make sure an owner '. 'of the package is involved and the commit message has '. 'a valid revision, reviewed by, and author.')) ->setOptions( array( 'disabled' => pht('Disabled'), 'enabled' => pht('Enabled'), )) ->setValue(($v_auditing ? 'enabled' : 'disabled'))) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setLabel(pht('Description')) ->setName('description') ->setValue($v_description)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($button_text)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if ($package->getID()) { $crumbs->addTextCrumb( $package->getName(), $this->getApplicationURI('package/'.$package->getID().'/')); $crumbs->addTextCrumb(pht('Edit')); } else { $crumbs->addTextCrumb(pht('New Package')); } return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php index f2691ea3b..74f77f79b 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php @@ -1,320 +1,397 @@ ownerPHIDs = $phids; return $this; } + /** + * Query owner authority. This will expand authorities, so a user PHID will + * match both packages they own directly and packages owned by a project they + * are a member of. + */ + public function withAuthorityPHIDs(array $phids) { + $this->authorityPHIDs = $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 withPaths(array $paths) { + $this->paths = $paths; + return $this; + } + public function withControl($repository_phid, array $paths) { if (empty($this->controlMap[$repository_phid])) { $this->controlMap[$repository_phid] = array(); } foreach ($paths as $path) { $path = (string)$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 needOwners($need_owners) { + $this->needOwners = $need_owners; + 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->needOwners) { + $package_ids = mpull($packages, 'getID'); + + $owners = id(new PhabricatorOwnersOwner())->loadAllWhere( + 'packageID IN (%Ld)', + $package_ids); + $owners = mgroup($owners, 'getPackageID'); + + foreach ($packages as $package) { + $package->attachOwners(idx($owners, $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) { + if ($this->shouldJoinOwnersTable()) { $joins[] = qsprintf( $conn, 'JOIN %T o ON o.packageID = p.id', id(new PhabricatorOwnersOwner())->getTableName()); } - if ($this->shouldJoinOwnersPathTable()) { + if ($this->shouldJoinPathTable()) { $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); + if ($this->authorityPHIDs !== null) { + $authority_phids = $this->expandAuthority($this->authorityPHIDs); + $where[] = qsprintf( + $conn, + 'o.userPHID IN (%Ls)', + $authority_phids); + } + if ($this->ownerPHIDs !== null) { $where[] = qsprintf( $conn, 'o.userPHID IN (%Ls)', - $all_phids); + $this->ownerPHIDs); + } + + if ($this->paths !== null) { + $where[] = qsprintf( + $conn, + 'rpath.path IN (%Ls)', + $this->getFragmentsForPaths($this->paths)); } 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; - } - } + $fragments = $this->getFragmentsForPaths($paths); $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->shouldJoinOwnersPathTable()) { + if ($this->shouldJoinOwnersTable()) { return true; } - if ($this->ownerPHIDs) { + if ($this->shouldJoinPathTable()) { 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() { + private function shouldJoinOwnersTable() { + if ($this->ownerPHIDs !== null) { + return true; + } + + if ($this->authorityPHIDs !== null) { + return true; + } + + return false; + } + + private function shouldJoinPathTable() { if ($this->repositoryPHIDs !== null) { return true; } + if ($this->paths !== null) { + return true; + } + if ($this->controlMap) { return true; } return false; } + private function expandAuthority(array $phids) { + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->withMemberPHIDs($phids) + ->execute(); + $project_phids = mpull($projects, 'getPHID'); + + return array_fuse($phids) + array_fuse($project_phids); + } + + private function getFragmentsForPaths(array $paths) { + $fragments = array(); + + foreach ($paths as $path) { + foreach (PhabricatorOwnersPackage::splitPath($path) as $fragment) { + $fragments[$fragment] = $fragment; + } + } + + return $fragments; + } + /* -( 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) { $path = (string)$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/query/PhabricatorOwnersPackageSearchEngine.php b/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php index fc2f8f620..a26a9b3c5 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php @@ -1,110 +1,118 @@ setLabel(pht('Owners')) - ->setKey('ownerPHIDs') - ->setAliases(array('owner', 'owners')) + ->setLabel(pht('Authority')) + ->setKey('authorityPHIDs') + ->setAliases(array('authority', 'authorities')) ->setDatasource(new PhabricatorProjectOrUserDatasource()), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Repositories')) ->setKey('repositoryPHIDs') ->setAliases(array('repository', 'repositories')) ->setDatasource(new DiffusionRepositoryDatasource()), + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Paths')) + ->setKey('paths') + ->setAliases(array('path')), ); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); - if ($map['ownerPHIDs']) { - $query->withOwnerPHIDs($map['ownerPHIDs']); + if ($map['authorityPHIDs']) { + $query->withAuthorityPHIDs($map['authorityPHIDs']); } if ($map['repositoryPHIDs']) { $query->withRepositoryPHIDs($map['repositoryPHIDs']); } + if ($map['paths']) { + $query->withPaths($map['paths']); + } + return $query; } protected function getURI($path) { return '/owners/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { - $names['owned'] = pht('Owned'); + $names['authority'] = pht('Owned'); } $names += array( 'all' => pht('All Packages'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; - case 'owned': + case 'authority': return $query->setParameter( - 'ownerPHIDs', + 'authorityPHIDs', array($this->requireViewer()->getPHID())); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $packages, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($packages, 'PhabricatorOwnersPackage'); $viewer = $this->requireViewer(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($packages as $package) { $id = $package->getID(); $item = id(new PHUIObjectItemView()) ->setObject($package) ->setObjectName(pht('Package %d', $id)) ->setHeader($package->getName()) ->setHref('/owners/package/'.$id.'/'); $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No packages found.')); return $result; } } diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index bfba5856c..7d0d8e65a 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -1,274 +1,287 @@ setAuditingEnabled(0); + ->setAuditingEnabled(0) + ->attachPaths(array()) + ->attachOwners(array()); } 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; } 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); } + public function attachOwners(array $owners) { + assert_instances_of($owners, 'PhabricatorOwnersOwner'); + $this->owners = $owners; + return $this; + } + + public function getOwners() { + return $this->assertAttached($this->owners); + } + /* -( 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; } }