diff --git a/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php index e06da5f4e3..8657981836 100644 --- a/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php +++ b/src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php @@ -1,111 +1,155 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function execute() { + $viewer = $this->getViewer(); + $phids = array_fuse($this->phids); $actors = array(); $type_map = array(); foreach ($phids as $phid) { $type_map[phid_get_type($phid)][] = $phid; } // TODO: Generalize this somewhere else. + + // If we have packages, break them down into their constituent user and + // project owners first. Then we'll resolve those and build the packages + // back up from the pieces. + $package_type = PhabricatorOwnersPackagePHIDType::TYPECONST; + $package_phids = idx($type_map, $package_type, array()); + unset($type_map[$package_type]); + + $package_map = array(); + if ($package_phids) { + $packages = id(new PhabricatorOwnersPackageQuery()) + ->setViewer($viewer) + ->withPHIDs($package_phids) + ->execute(); + + foreach ($packages as $package) { + $package_owners = array(); + foreach ($package->getOwners() as $owner) { + $owner_phid = $owner->getUserPHID(); + $owner_type = phid_get_type($owner_phid); + $type_map[$owner_type][] = $owner_phid; + $package_owners[] = $owner_phid; + } + $package_map[$package->getPHID()] = $package_owners; + } + } + $results = array(); foreach ($type_map as $type => $phids) { switch ($type) { case PhabricatorProjectProjectPHIDType::TYPECONST: // NOTE: We're loading the projects here in order to respect policies. $projects = id(new PhabricatorProjectQuery()) - ->setViewer($this->getViewer()) + ->setViewer($viewer) ->withPHIDs($phids) ->needMembers(true) ->needWatchers(true) ->execute(); $edge_type = PhabricatorProjectSilencedEdgeType::EDGECONST; $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes( array( $edge_type, )); $edge_query->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($phids as $phid) { $project = idx($projects, $phid); if (!$project) { $results[$phid] = array(); continue; } // Recipients are members who haven't silenced the project, plus // watchers. $members = $project->getMemberPHIDs(); $members = array_fuse($members); $watchers = $project->getWatcherPHIDs(); $watchers = array_fuse($watchers); $silenced = $edge_query->getDestinationPHIDs( array($phid), array($edge_type)); $silenced = array_fuse($silenced); $result_map = array_diff_key($members, $silenced); $result_map = $result_map + $watchers; $results[$phid] = array_values($result_map); } break; default: // For other types, just map the PHID to itself without modification. // This allows callers to do less work. foreach ($phids as $phid) { $results[$phid] = array($phid); } break; } } + // For any packages, stitch them back together from the resolved users + // and projects. + if ($package_map) { + foreach ($package_map as $package_phid => $owner_phids) { + $resolved = array(); + foreach ($owner_phids as $owner_phid) { + $resolved_phids = idx($results, $owner_phid, array()); + foreach ($resolved_phids as $resolved_phid) { + $resolved[] = $resolved_phid; + } + } + $results[$package_phid] = $resolved; + } + } + return $results; } /** * Execute the query, merging results into a single list of unique member * PHIDs. */ public function executeExpansion() { return array_unique(array_mergev($this->execute())); } } diff --git a/src/applications/metamta/replyhandler/PhabricatorMailTarget.php b/src/applications/metamta/replyhandler/PhabricatorMailTarget.php index e7e79c5ef9..c607087b22 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailTarget.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailTarget.php @@ -1,146 +1,150 @@ rawToPHIDs = $to_phids; return $this; } public function setRawCCPHIDs(array $cc_phids) { $this->rawCCPHIDs = $cc_phids; return $this; } public function setCCMap(array $cc_map) { $this->ccMap = $cc_map; return $this; } public function getCCMap() { return $this->ccMap; } public function setToMap(array $to_map) { $this->toMap = $to_map; return $this; } public function getToMap() { return $this->toMap; } public function setReplyTo($reply_to) { $this->replyTo = $reply_to; return $this; } public function getReplyTo() { return $this->replyTo; } public function setViewer($viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function willSendMail(PhabricatorMetaMTAMail $mail) { $viewer = $this->getViewer(); $mail->addPHIDHeaders('X-Phabricator-To', $this->rawToPHIDs); $mail->addPHIDHeaders('X-Phabricator-Cc', $this->rawCCPHIDs); $to_handles = $viewer->loadHandles($this->rawToPHIDs); $cc_handles = $viewer->loadHandles($this->rawCCPHIDs); $body = $mail->getBody(); $body .= "\n"; $body .= $this->getRecipientsSummary($to_handles, $cc_handles); $mail->setBody($body); $html_body = $mail->getHTMLBody(); if (strlen($html_body)) { $html_body .= hsprintf( '%s', $this->getRecipientsSummaryHTML($to_handles, $cc_handles)); } $mail->setHTMLBody($html_body); $reply_to = $this->getReplyTo(); if ($reply_to) { $mail->setReplyTo($reply_to); } $to = array_keys($this->getToMap()); if ($to) { $mail->addTos($to); } $cc = array_keys($this->getCCMap()); if ($cc) { $mail->addCCs($cc); } return $mail; } private function getRecipientsSummary( PhabricatorHandleList $to_handles, PhabricatorHandleList $cc_handles) { if (!PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) { return ''; } $to_handles = iterator_to_array($to_handles); $cc_handles = iterator_to_array($cc_handles); $body = ''; + if ($to_handles) { - $body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n"; + $to_names = mpull($to_handles, 'getCommandLineObjectName'); + $body .= "To: ".implode(', ', $to_names)."\n"; } + if ($cc_handles) { - $body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n"; + $cc_names = mpull($cc_handles, 'getCommandLineObjectName'); + $body .= "Cc: ".implode(', ', $cc_names)."\n"; } return $body; } private function getRecipientsSummaryHTML( PhabricatorHandleList $to_handles, PhabricatorHandleList $cc_handles) { if (!PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) { return ''; } $to_handles = iterator_to_array($to_handles); $cc_handles = iterator_to_array($cc_handles); $body = array(); if ($to_handles) { $body[] = phutil_tag('strong', array(), 'To: '); $body[] = phutil_implode_html(', ', mpull($to_handles, 'getName')); $body[] = phutil_tag('br'); } if ($cc_handles) { $body[] = phutil_tag('strong', array(), 'Cc: '); $body[] = phutil_implode_html(', ', mpull($cc_handles, 'getName')); $body[] = phutil_tag('br'); } return phutil_tag('div', array(), $body); } } diff --git a/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php b/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php index 5fa7492fef..2e0e03bb3c 100644 --- a/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php +++ b/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php @@ -1,25 +1,26 @@ getUser(); $package = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, 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().'/'); + ->setURI($package->getURI()); } else { $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, 'getDisplayName', '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().'/'; + $cancel_uri = $package->getURI(); $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'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Paths')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $package->getName(), $this->getApplicationURI('package/'.$package->getID().'/')); $crumbs->addTextCrumb(pht('Edit Paths')); $crumbs->setBorder(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit Paths: %s', $package->getName())) ->setHeaderIcon('fa-pencil'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($box); $title = array($package->getName(), pht('Edit Paths')); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } } diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php index fc97be6fce..7f705ec13d 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php @@ -1,147 +1,146 @@ getViewer()); } protected function newObjectQuery() { return id(new PhabricatorOwnersPackageQuery()) ->needPaths(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Package'); } protected function getObjectEditTitleText($object) { return pht('Edit Package: %s', $object->getName()); } protected function getObjectEditShortText($object) { return pht('Package %d', $object->getID()); } protected function getObjectCreateShortText() { return pht('Create Package'); } protected function getObjectName() { return pht('Package'); } protected function getObjectViewURI($object) { - $id = $object->getID(); - return "/owners/package/{$id}/"; + return $object->getURI(); } protected function buildCustomEditFields($object) { $paths_help = pht(<<setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the package.')) ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_NAME) ->setIsRequired(true) ->setValue($object->getName()), id(new PhabricatorDatasourceEditField()) ->setKey('owners') ->setLabel(pht('Owners')) ->setDescription(pht('Users and projects which own the package.')) ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_OWNERS) ->setDatasource(new PhabricatorProjectOrUserDatasource()) ->setIsCopyable(true) ->setValue($object->getOwnerPHIDs()), id(new PhabricatorSelectEditField()) ->setKey('auditing') ->setLabel(pht('Auditing')) ->setDescription( pht( 'Automatically trigger audits for commits affecting files in '. 'this package.')) ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_AUDITING) ->setIsCopyable(true) ->setValue($object->getAuditingEnabled()) ->setOptions( array( '' => pht('Disabled'), '1' => pht('Enabled'), )), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setDescription(pht('Human-readable description of the package.')) ->setTransactionType( PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION) ->setValue($object->getDescription()), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setDescription(pht('Archive or enable the package.')) ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_STATUS) ->setIsConduitOnly(true) ->setValue($object->getStatus()) ->setOptions($object->getStatusNameMap()), id(new PhabricatorConduitEditField()) ->setKey('paths.set') ->setLabel(pht('Paths')) ->setIsConduitOnly(true) ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_PATHS) ->setConduitDescription( pht('Overwrite existing package paths with new paths.')) ->setConduitTypeDescription( pht('List of dictionaries, each describing a path.')) ->setConduitDocumentation($paths_help), ); } } diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php index 52f30e0994..f9267a0ba9 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php @@ -1,363 +1,362 @@ getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: return $object->getName(); case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: $phids = mpull($object->getOwners(), '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: $paths = $object->getPaths(); return mpull($paths, 'getRef'); case PhabricatorOwnersPackageTransaction::TYPE_STATUS: return $object->getStatus(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: case PhabricatorOwnersPackageTransaction::TYPE_STATUS: return $xaction->getNewValue(); case PhabricatorOwnersPackageTransaction::TYPE_PATHS: $new = $xaction->getNewValue(); foreach ($new as $key => $info) { $new[$key]['excluded'] = (int)idx($info, 'excluded'); } return $new; 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_DESCRIPTION: $object->setDescription($xaction->getNewValue()); return; case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: $object->setAuditingEnabled($xaction->getNewValue()); return; case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: case PhabricatorOwnersPackageTransaction::TYPE_PATHS: return; case PhabricatorOwnersPackageTransaction::TYPE_STATUS: $object->setStatus($xaction->getNewValue()); return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorOwnersPackageTransaction::TYPE_NAME: case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: case PhabricatorOwnersPackageTransaction::TYPE_STATUS: return; case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $owners = $object->getOwners(); $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(); $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; } foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); if (preg_match('([,!])', $new)) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( 'Package names may not contain commas (",") or exclamation '. 'marks ("!"). These characters are ambiguous when package '. 'names are parsed from the command line.'), $xaction); } } break; case PhabricatorOwnersPackageTransaction::TYPE_PATHS: if (!$xactions) { continue; } $old = mpull($object->getPaths(), 'getRef'); foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); // Check that we have a list of paths. if (!is_array($new)) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('Path specification must be a list of paths.'), $xaction); continue; } // Check that each item in the list is formatted properly. $type_exception = null; foreach ($new as $key => $value) { try { PhutilTypeSpec::checkMap( $value, array( 'repositoryPHID' => 'string', 'path' => 'string', 'excluded' => 'optional wild', )); } catch (PhutilTypeCheckException $ex) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( 'Path specification list contains invalid value '. 'in key "%s": %s.', $key, $ex->getMessage()), $xaction); $type_exception = $ex; } } if ($type_exception) { continue; } // Check that any new paths reference legitimate repositories which // the viewer has permission to see. list($rem, $add) = PhabricatorOwnersPath::getTransactionValueChanges( $old, $new); if ($add) { $repository_phids = ipull($add, 'repositoryPHID'); $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getActor()) ->withPHIDs($repository_phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($add as $ref) { $repository_phid = $ref['repositoryPHID']; if (isset($repositories[$repository_phid])) { continue; } $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( 'Path specification list references repository PHID "%s", '. 'but that is not a valid, visible repository.', $repository_phid)); } } } break; } return $errors; } 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( $this->requireActor()->getPHID(), ); } protected function getMailCC(PhabricatorLiskDAO $object) { return mpull($object->getOwners(), '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().'/'); + $detail_uri = PhabricatorEnv::getProductionURI($object->getURI()); $body->addLinkSection( pht('PACKAGE DETAIL'), $detail_uri); return $body; } protected function supportsSearch() { return true; } } diff --git a/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php b/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php index 772bb84fc0..cb23ca4041 100644 --- a/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php +++ b/src/applications/owners/phid/PhabricatorOwnersPackagePHIDType.php @@ -1,84 +1,85 @@ withPHIDs($phids); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { foreach ($handles as $phid => $handle) { $package = $objects[$phid]; $monogram = $package->getMonogram(); $name = $package->getName(); $id = $package->getID(); + $uri = $package->getURI(); $handle ->setName($monogram) ->setFullName("{$monogram}: {$name}") ->setCommandLineObjectName("{$monogram} {$name}") - ->setURI("/owners/package/{$id}/"); + ->setURI($uri); if ($package->isArchived()) { $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); } } } public function canLoadNamedObject($name) { return preg_match('/^O\d*[1-9]\d*$/i', $name); } public function loadNamedObjects( PhabricatorObjectQuery $query, array $names) { $id_map = array(); foreach ($names as $name) { $id = (int)substr($name, 1); $id_map[$id][] = $name; } $objects = id(new PhabricatorOwnersPackageQuery()) ->setViewer($query->getViewer()) ->withIDs(array_keys($id_map)) ->execute(); $results = array(); foreach ($objects as $id => $object) { foreach (idx($id_map, $id, array()) as $name) { $results[$name] = $object; } } return $results; } } diff --git a/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php b/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php index e74d3c6a63..728c3f42a8 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageSearchEngine.php @@ -1,178 +1,178 @@ setLabel(pht('Authority')) ->setKey('authorityPHIDs') ->setAliases(array('authority', 'authorities')) ->setConduitKey('owners') ->setDescription( pht('Search for packages with specific owners.')) ->setDatasource(new PhabricatorProjectOrUserDatasource()), id(new PhabricatorSearchTextField()) ->setLabel(pht('Name Contains')) ->setKey('name') ->setDescription(pht('Search for packages by name substrings.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Repositories')) ->setKey('repositoryPHIDs') ->setConduitKey('repositories') ->setAliases(array('repository', 'repositories')) ->setDescription( pht('Search for packages by included repositories.')) ->setDatasource(new DiffusionRepositoryDatasource()), id(new PhabricatorSearchStringListField()) ->setLabel(pht('Paths')) ->setKey('paths') ->setAliases(array('path')) ->setDescription( pht('Search for packages affecting specific paths.')), id(new PhabricatorSearchCheckboxesField()) ->setKey('statuses') ->setLabel(pht('Status')) ->setDescription( pht('Search for active or archived packages.')) ->setOptions( id(new PhabricatorOwnersPackage()) ->getStatusNameMap()), ); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); if ($map['authorityPHIDs']) { $query->withAuthorityPHIDs($map['authorityPHIDs']); } if ($map['repositoryPHIDs']) { $query->withRepositoryPHIDs($map['repositoryPHIDs']); } if ($map['paths']) { $query->withPaths($map['paths']); } if ($map['statuses']) { $query->withStatuses($map['statuses']); } if (strlen($map['name'])) { $query->withNameNgrams($map['name']); } return $query; } protected function getURI($path) { return '/owners/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['authority'] = pht('Owned'); } $names += array( 'active' => pht('Active Packages'), '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 'active': return $query->setParameter( 'statuses', array( PhabricatorOwnersPackage::STATUS_ACTIVE, )); case 'authority': return $query->setParameter( '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($package->getMonogram()) ->setHeader($package->getName()) - ->setHref('/owners/package/'.$id.'/'); + ->setHref($package->getURI()); if ($package->isArchived()) { $item->setDisabled(true); } $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No packages found.')); return $result; } protected function getNewUserBody() { $create_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Create a Package')) ->setHref('/owners/edit/') ->setColor(PHUIButtonView::GREEN); $icon = $this->getApplication()->getIcon(); $app_name = $this->getApplication()->getName(); $view = id(new PHUIBigInfoView()) ->setIcon($icon) ->setTitle(pht('Welcome to %s', $app_name)) ->setDescription( pht('Group sections of a codebase into packages for re-use in other '. 'areas of Phabricator, like Herald rules.')) ->addAction($create_button); return $view; } } diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index 83c007e170..14eb618c30 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -1,468 +1,472 @@ setViewer($actor) ->withClasses(array('PhabricatorOwnersApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorOwnersDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( PhabricatorOwnersDefaultEditCapability::CAPABILITY); return id(new PhabricatorOwnersPackage()) ->setAuditingEnabled(0) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->attachPaths(array()) ->setStatus(self::STATUS_ACTIVE) ->attachOwners(array()) ->setDescription(''); } public static function getStatusNameMap() { return array( self::STATUS_ACTIVE => pht('Active'), self::STATUS_ARCHIVED => pht('Archived'), ); } 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' => 'sort128', 'originalName' => 'text255', 'description' => 'text', 'primaryOwnerPHID' => 'phid?', 'auditingEnabled' => 'bool', 'mailKey' => 'bytes20', 'status' => 'text32', ), ) + 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 isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } 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) { $trailing_slash = preg_match('@/$@', $path) ? '/' : ''; $path = trim($path, '/'); $parts = explode('/', $path); $result = array(); while (count($parts)) { $result[] = '/'.implode('/', $parts).$trailing_slash; $trailing_slash = '/'; array_pop($parts); } $result[] = '/'; return array_reverse($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); } public function getOwnerPHIDs() { return mpull($this->getOwners(), 'getUserPHID'); } public function isOwnerPHID($phid) { if (!$phid) { return false; } $owner_phids = $this->getOwnerPHIDs(); $owner_phids = array_fuse($owner_phids); return isset($owner_phids[$phid]); } public function getMonogram() { return 'O'.$this->getID(); } + public function getURI() { + // TODO: Move these to "/O123" for consistency. + return '/owners/package/'.$this->getID().'/'; + } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isOwnerPHID($viewer->getPHID())) { return true; } break; } return false; } public function describeAutomaticCapability($capability) { return pht('Owners of a package may always view it.'); } /* -( 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; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('owners.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorOwnersCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE packageID = %d', id(new PhabricatorOwnersPath())->getTableName(), $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE packageID = %d', id(new PhabricatorOwnersOwner())->getTableName(), $this->getID()); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the package.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('string') ->setDescription(pht('The package description.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Active or archived status of the package.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('owners') ->setType('list>') ->setDescription(pht('List of package owners.')), ); } public function getFieldValuesForConduit() { $owner_list = array(); foreach ($this->getOwners() as $owner) { $owner_list[] = array( 'ownerPHID' => $owner->getUserPHID(), ); } return array( 'name' => $this->getName(), 'description' => $this->getDescription(), 'status' => $this->getStatus(), 'owners' => $owner_list, ); } public function getConduitSearchAttachments() { return array( id(new PhabricatorOwnersPathsSearchEngineAttachment()) ->setAttachmentKey('paths'), ); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorOwnersPackageFulltextEngine(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new PhabricatorOwnersPackageNameNgrams()) ->setValue($this->getName()), ); } } diff --git a/src/applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php b/src/applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php index 9230ce270e..41d5d6823a 100644 --- a/src/applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php +++ b/src/applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php @@ -1,39 +1,42 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $query = id(new PhabricatorOwnersPackageQuery()) ->withNameNgrams($raw_query) ->setOrder('name'); $packages = $this->executeQuery($query); foreach ($packages as $package) { + $name = $package->getName(); + $monogram = $package->getMonogram(); + $results[] = id(new PhabricatorTypeaheadResult()) - ->setName($package->getName()) - ->setURI('/owners/package/'.$package->getID().'/') + ->setName("{$monogram}: {$name}") + ->setURI($package->getURI()) ->setPHID($package->getPHID()); } return $this->filterResultsAgainstTokens($results); } }