diff --git a/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php b/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php index f3858afbf8..6d29d2345d 100644 --- a/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php +++ b/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php @@ -1,124 +1,124 @@ formatStringConstants($statuses); return array( 'auditorPHIDs' => 'optional list', 'commitPHIDs' => 'optional list', 'status' => ('optional '.$status_const. ' (default = "audit-status-any")'), 'offset' => 'optional int', 'limit' => 'optional int (default = 100)', ); } protected function defineReturnType() { return 'list'; } protected function execute(ConduitAPIRequest $request) { $query = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->needAuditRequests(true); $auditor_phids = $request->getValue('auditorPHIDs', array()); if ($auditor_phids) { $query->withAuditorPHIDs($auditor_phids); } $commit_phids = $request->getValue('commitPHIDs', array()); if ($commit_phids) { $query->withPHIDs($commit_phids); } $status_map = array( self::AUDIT_LEGACYSTATUS_OPEN => array( - PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT, - PhabricatorAuditCommitStatusConstants::CONCERN_RAISED, + PhabricatorAuditCommitStatusConstants::MODERN_NEEDS_AUDIT, + PhabricatorAuditCommitStatusConstants::MODERN_CONCERN_RAISED, ), self::AUDIT_LEGACYSTATUS_CONCERN => array( - PhabricatorAuditCommitStatusConstants::CONCERN_RAISED, + PhabricatorAuditCommitStatusConstants::MODERN_CONCERN_RAISED, ), self::AUDIT_LEGACYSTATUS_ACCEPTED => array( - PhabricatorAuditCommitStatusConstants::FULLY_AUDITED, + PhabricatorAuditCommitStatusConstants::MODERN_AUDITED, ), self::AUDIT_LEGACYSTATUS_PARTIAL => array( - PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED, + PhabricatorAuditCommitStatusConstants::MODERN_PARTIALLY_AUDITED, ), ); $status = $request->getValue('status'); if (isset($status_map[$status])) { $query->withStatuses($status_map[$status]); } // NOTE: These affect the number of commits identified, which is sort of // reasonable but means the method may return an arbitrary number of // actual audit requests. $query->setOffset($request->getValue('offset', 0)); $query->setLimit($request->getValue('limit', 100)); $commits = $query->execute(); $auditor_map = array_fuse($auditor_phids); $results = array(); foreach ($commits as $commit) { $requests = $commit->getAudits(); foreach ($requests as $request) { // If this audit isn't triggered for one of the requested PHIDs, // skip it. if ($auditor_map && empty($auditor_map[$request->getAuditorPHID()])) { continue; } $results[] = array( 'id' => $request->getID(), 'commitPHID' => $request->getCommitPHID(), 'auditorPHID' => $request->getAuditorPHID(), 'reasons' => array(), 'status' => $request->getAuditStatus(), ); } } return $results; } } diff --git a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php index 50e4180074..2196940545 100644 --- a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php +++ b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php @@ -1,164 +1,174 @@ $spec) { - if (idx($spec, 'legacy') == $status) { - return self::newForStatus($key); + if (is_int($status) || ctype_digit($status)) { + foreach ($map as $key => $spec) { + if ((int)idx($spec, 'legacy') === (int)$status) { + return self::newForStatus($key); + } } } return self::newForStatus($status); } public static function newForStatus($status) { $result = new self(); $result->key = $status; $map = self::getMap(); if (isset($map[$status])) { $result->spec = $map[$status]; } return $result; } public function getKey() { return $this->key; } public function getIcon() { return idx($this->spec, 'icon'); } public function getColor() { return idx($this->spec, 'color'); } + public function getLegacyKey() { + return idx($this->spec, 'legacy'); + } + public function getName() { return idx($this->spec, 'name', pht('Unknown ("%s")', $this->key)); } public function isNoAudit() { return ($this->key == self::MODERN_NONE); } + public function isNeedsAudit() { + return ($this->key == self::MODERN_NEEDS_AUDIT); + } + public function isConcernRaised() { return ($this->key == self::MODERN_CONCERN_RAISED); } public function isNeedsVerification() { return ($this->key == self::MODERN_NEEDS_VERIFICATION); } public function isPartiallyAudited() { return ($this->key == self::MODERN_PARTIALLY_AUDITED); } public function isAudited() { return ($this->key == self::MODERN_AUDITED); } public function getIsClosed() { return idx($this->spec, 'closed'); } public static function getStatusNameMap() { $map = self::getMap(); return ipull($map, 'name', 'legacy'); } public static function getStatusName($code) { return idx(self::getStatusNameMap(), $code, pht('Unknown')); } public static function getOpenStatusConstants() { $constants = array(); foreach (self::getMap() as $map) { if (!$map['closed']) { $constants[] = $map['legacy']; } } return $constants; } public static function getStatusColor($code) { $map = self::getMap(); $map = ipull($map, 'color', 'legacy'); return idx($map, $code); } public static function getStatusIcon($code) { $map = self::getMap(); $map = ipull($map, 'icon', 'legacy'); return idx($map, $code); } private static function getMap() { return array( self::MODERN_NONE => array( 'name' => pht('No Audits'), 'legacy' => self::NONE, 'icon' => 'fa-check', 'color' => 'bluegrey', 'closed' => true, ), self::MODERN_NEEDS_AUDIT => array( 'name' => pht('Audit Required'), 'legacy' => self::NEEDS_AUDIT, 'icon' => 'fa-exclamation-circle', 'color' => 'orange', 'closed' => false, ), self::MODERN_CONCERN_RAISED => array( 'name' => pht('Concern Raised'), 'legacy' => self::CONCERN_RAISED, 'icon' => 'fa-times-circle', 'color' => 'red', 'closed' => false, ), self::MODERN_PARTIALLY_AUDITED => array( 'name' => pht('Partially Audited'), 'legacy' => self::PARTIALLY_AUDITED, 'icon' => 'fa-check-circle-o', 'color' => 'yellow', 'closed' => false, ), self::MODERN_AUDITED => array( 'name' => pht('Audited'), 'legacy' => self::FULLY_AUDITED, 'icon' => 'fa-check-circle', 'color' => 'green', 'closed' => true, ), self::MODERN_NEEDS_VERIFICATION => array( 'name' => pht('Needs Verification'), 'legacy' => self::NEEDS_VERIFICATION, 'icon' => 'fa-refresh', 'color' => 'indigo', 'closed' => false, ), ); } } diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index 0ca9cd97d4..9a7e21b0af 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -1,849 +1,856 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } /** * Load commits by partial or full identifiers, e.g. "rXab82393", "rX1234", * or "a9caf12". When an identifier matches multiple commits, they will all * be returned; callers should be prepared to deal with more results than * they queried for. */ public function withIdentifiers(array $identifiers) { // Some workflows (like blame lookups) can pass in large numbers of // duplicate identifiers. We only care about unique identifiers, so // get rid of duplicates immediately. $identifiers = array_fuse($identifiers); $this->identifiers = $identifiers; return $this; } /** * Look up commits in a specific repository. This is a shorthand for calling * @{method:withDefaultRepository} and @{method:withRepositoryIDs}. */ public function withRepository(PhabricatorRepository $repository) { $this->withDefaultRepository($repository); $this->withRepositoryIDs(array($repository->getID())); return $this; } /** * Look up commits in a specific repository. Prefer * @{method:withRepositoryIDs}; the underlying table is keyed by ID such * that this method requires a separate initial query to map PHID to ID. */ public function withRepositoryPHIDs(array $phids) { $this->repositoryPHIDs = $phids; return $this; } /** * If a default repository is provided, ambiguous commit identifiers will * be assumed to belong to the default repository. * * For example, "r123" appearing in a commit message in repository X is * likely to be unambiguously "rX123". Normally the reference would be * considered ambiguous, but if you provide a default repository it will * be correctly resolved. */ public function withDefaultRepository(PhabricatorRepository $repository) { $this->defaultRepository = $repository; return $this; } public function withRepositoryIDs(array $repository_ids) { $this->repositoryIDs = array_unique($repository_ids); return $this; } public function needCommitData($need) { $this->needCommitData = $need; return $this; } public function needDrafts($need) { $this->needDrafts = $need; return $this; } public function needIdentities($need) { $this->needIdentities = $need; return $this; } public function needAuditRequests($need) { $this->needAuditRequests = $need; return $this; } public function withAuditIDs(array $ids) { $this->auditIDs = $ids; return $this; } public function withAuditorPHIDs(array $auditor_phids) { $this->auditorPHIDs = $auditor_phids; return $this; } public function withResponsiblePHIDs(array $responsible_phids) { $this->responsiblePHIDs = $responsible_phids; return $this; } public function withPackagePHIDs(array $package_phids) { $this->packagePHIDs = $package_phids; return $this; } public function withUnreachable($unreachable) { $this->unreachable = $unreachable; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withEpochRange($min, $max) { $this->epochMin = $min; $this->epochMax = $max; return $this; } public function withImporting($importing) { $this->importing = $importing; return $this; } public function withAncestorsOf(array $refs) { $this->ancestorsOf = $refs; return $this; } public function getIdentifierMap() { if ($this->identifierMap === null) { throw new Exception( pht( 'You must %s the query before accessing the identifier map.', 'execute()')); } return $this->identifierMap; } protected function getPrimaryTableAlias() { return 'commit'; } protected function willExecute() { if ($this->identifierMap === null) { $this->identifierMap = array(); } } public function newResultObject() { return new PhabricatorRepositoryCommit(); } protected function loadPage() { $table = $this->newResultObject(); $conn = $table->establishConnection('r'); $subqueries = array(); if ($this->responsiblePHIDs) { $base_authors = $this->authorPHIDs; $base_auditors = $this->auditorPHIDs; $responsible_phids = $this->responsiblePHIDs; if ($base_authors) { $all_authors = array_merge($base_authors, $responsible_phids); } else { $all_authors = $responsible_phids; } if ($base_auditors) { $all_auditors = array_merge($base_auditors, $responsible_phids); } else { $all_auditors = $responsible_phids; } $this->authorPHIDs = $all_authors; $this->auditorPHIDs = $base_auditors; $subqueries[] = $this->buildStandardPageQuery( $conn, $table->getTableName()); $this->authorPHIDs = $base_authors; $this->auditorPHIDs = $all_auditors; $subqueries[] = $this->buildStandardPageQuery( $conn, $table->getTableName()); } else { $subqueries[] = $this->buildStandardPageQuery( $conn, $table->getTableName()); } if (count($subqueries) > 1) { foreach ($subqueries as $key => $subquery) { $subqueries[$key] = '('.$subquery.')'; } $query = qsprintf( $conn, '%Q %Q %Q', implode(' UNION DISTINCT ', $subqueries), $this->buildOrderClause($conn, true), $this->buildLimitClause($conn)); } else { $query = head($subqueries); } $rows = queryfx_all($conn, '%Q', $query); $rows = $this->didLoadRawRows($rows); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $commits) { $repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIDs($repository_ids) ->execute(); $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; $result = array(); foreach ($commits as $key => $commit) { $repo = idx($repos, $commit->getRepositoryID()); if ($repo) { $commit->attachRepository($repo); } else { $this->didRejectResult($commit); unset($commits[$key]); continue; } // Build the identifierMap if ($this->identifiers !== null) { $ids = $this->identifiers; $prefixes = array( 'r'.$commit->getRepository()->getCallsign(), 'r'.$commit->getRepository()->getCallsign().':', 'R'.$commit->getRepository()->getID().':', '', // No prefix is valid too and will only match the commitIdentifier ); $suffix = $commit->getCommitIdentifier(); if ($commit->getRepository()->isSVN()) { foreach ($prefixes as $prefix) { if (isset($ids[$prefix.$suffix])) { $result[$prefix.$suffix][] = $commit; } } } else { // This awkward construction is so we can link the commits up in O(N) // time instead of O(N^2). for ($ii = $min_qualified; $ii <= strlen($suffix); $ii++) { $part = substr($suffix, 0, $ii); foreach ($prefixes as $prefix) { if (isset($ids[$prefix.$part])) { $result[$prefix.$part][] = $commit; } } } } } } if ($result) { foreach ($result as $identifier => $matching_commits) { if (count($matching_commits) == 1) { $result[$identifier] = head($matching_commits); } else { // This reference is ambiguous (it matches more than one commit) so // don't link it. unset($result[$identifier]); } } $this->identifierMap += $result; } return $commits; } protected function didFilterPage(array $commits) { $viewer = $this->getViewer(); if ($this->mustFilterRefs) { // If this flag is set, the query has an "Ancestors Of" constraint and // at least one of the constraining refs had too many ancestors for us // to apply the constraint with a big "commitIdentifier IN (%Ls)" clause. // We're going to filter each page and hope we get a full result set // before the query overheats. $ancestor_list = mpull($commits, 'getCommitIdentifier'); $ancestor_list = array_values($ancestor_list); foreach ($this->ancestorsOf as $ref) { try { $ancestor_list = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, DiffusionRequest::newFromDictionary( array( 'repository' => $this->refRepository, 'user' => $viewer, )), 'diffusion.internal.ancestors', array( 'ref' => $ref, 'commits' => $ancestor_list, )); } catch (ConduitClientException $ex) { throw new PhabricatorSearchConstraintException( $ex->getMessage()); } if (!$ancestor_list) { break; } } $ancestor_list = array_fuse($ancestor_list); foreach ($commits as $key => $commit) { $identifier = $commit->getCommitIdentifier(); if (!isset($ancestor_list[$identifier])) { $this->didRejectResult($commit); unset($commits[$key]); } } if (!$commits) { return $commits; } } if ($this->needCommitData) { $data = id(new PhabricatorRepositoryCommitData())->loadAllWhere( 'commitID in (%Ld)', mpull($commits, 'getID')); $data = mpull($data, null, 'getCommitID'); foreach ($commits as $commit) { $commit_data = idx($data, $commit->getID()); if (!$commit_data) { $commit_data = new PhabricatorRepositoryCommitData(); } $commit->attachCommitData($commit_data); } } if ($this->needAuditRequests) { $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere( 'commitPHID IN (%Ls)', mpull($commits, 'getPHID')); $requests = mgroup($requests, 'getCommitPHID'); foreach ($commits as $commit) { $audit_requests = idx($requests, $commit->getPHID(), array()); $commit->attachAudits($audit_requests); foreach ($audit_requests as $audit_request) { $audit_request->attachCommit($commit); } } } if ($this->needIdentities) { $identity_phids = array_merge( mpull($commits, 'getAuthorIdentityPHID'), mpull($commits, 'getCommitterIdentityPHID')); $data = id(new PhabricatorRepositoryIdentityQuery()) ->withPHIDs($identity_phids) ->setViewer($this->getViewer()) ->execute(); $data = mpull($data, null, 'getPHID'); foreach ($commits as $commit) { $author_identity = idx($data, $commit->getAuthorIdentityPHID()); $committer_identity = idx($data, $commit->getCommitterIdentityPHID()); $commit->attachIdentities($author_identity, $committer_identity); } } if ($this->needDrafts) { PhabricatorDraftEngine::attachDrafts( $viewer, $commits); } return $commits; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->repositoryPHIDs !== null) { $map_repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withPHIDs($this->repositoryPHIDs) ->execute(); if (!$map_repositories) { throw new PhabricatorEmptyQueryException(); } $repository_ids = mpull($map_repositories, 'getID'); if ($this->repositoryIDs !== null) { $repository_ids = array_merge($repository_ids, $this->repositoryIDs); } $this->withRepositoryIDs($repository_ids); } if ($this->ancestorsOf !== null) { if (count($this->repositoryIDs) !== 1) { throw new PhabricatorSearchConstraintException( pht( 'To search for commits which are ancestors of particular refs, '. 'you must constrain the search to exactly one repository.')); } $repository_id = head($this->repositoryIDs); $history_limit = $this->getRawResultLimit() * 32; $viewer = $this->getViewer(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withIDs(array($repository_id)) ->executeOne(); if (!$repository) { throw new PhabricatorEmptyQueryException(); } if ($repository->isSVN()) { throw new PhabricatorSearchConstraintException( pht( 'Subversion does not support searching for ancestors of '. 'a particular ref. This operation is not meaningful in '. 'Subversion.')); } if ($repository->isHg()) { throw new PhabricatorSearchConstraintException( pht( 'Mercurial does not currently support searching for ancestors of '. 'a particular ref.')); } $can_constrain = true; $history_identifiers = array(); foreach ($this->ancestorsOf as $key => $ref) { try { $raw_history = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, DiffusionRequest::newFromDictionary( array( 'repository' => $repository, 'user' => $viewer, )), 'diffusion.historyquery', array( 'commit' => $ref, 'limit' => $history_limit, )); } catch (ConduitClientException $ex) { throw new PhabricatorSearchConstraintException( $ex->getMessage()); } $ref_identifiers = array(); foreach ($raw_history['pathChanges'] as $change) { $ref_identifiers[] = $change['commitIdentifier']; } // If this ref had fewer total commits than the limit, we're safe to // apply the constraint as a large `IN (...)` query for a list of // commit identifiers. This is efficient. if ($history_limit) { if (count($ref_identifiers) >= $history_limit) { $can_constrain = false; break; } } $history_identifiers += array_fuse($ref_identifiers); } // If all refs had a small number of ancestors, we can just put the // constraint into the query here and we're done. Otherwise, we need // to filter each page after it comes out of the MySQL layer. if ($can_constrain) { $where[] = qsprintf( $conn, 'commit.commitIdentifier IN (%Ls)', $history_identifiers); } else { $this->mustFilterRefs = true; $this->refRepository = $repository; } } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'commit.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'commit.phid IN (%Ls)', $this->phids); } if ($this->repositoryIDs !== null) { $where[] = qsprintf( $conn, 'commit.repositoryID IN (%Ld)', $this->repositoryIDs); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'commit.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->epochMin !== null) { $where[] = qsprintf( $conn, 'commit.epoch >= %d', $this->epochMin); } if ($this->epochMax !== null) { $where[] = qsprintf( $conn, 'commit.epoch <= %d', $this->epochMax); } if ($this->importing !== null) { if ($this->importing) { $where[] = qsprintf( $conn, '(commit.importStatus & %d) != %d', PhabricatorRepositoryCommit::IMPORTED_ALL, PhabricatorRepositoryCommit::IMPORTED_ALL); } else { $where[] = qsprintf( $conn, '(commit.importStatus & %d) = %d', PhabricatorRepositoryCommit::IMPORTED_ALL, PhabricatorRepositoryCommit::IMPORTED_ALL); } } if ($this->identifiers !== null) { $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH; $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; $refs = array(); $bare = array(); foreach ($this->identifiers as $identifier) { $matches = null; preg_match('/^(?:[rR]([A-Z]+:?|[0-9]+:))?(.*)$/', $identifier, $matches); $repo = nonempty(rtrim($matches[1], ':'), null); $commit_identifier = nonempty($matches[2], null); if ($repo === null) { if ($this->defaultRepository) { $repo = $this->defaultRepository->getPHID(); } } if ($repo === null) { if (strlen($commit_identifier) < $min_unqualified) { continue; } $bare[] = $commit_identifier; } else { $refs[] = array( 'repository' => $repo, 'identifier' => $commit_identifier, ); } } $sql = array(); foreach ($bare as $identifier) { $sql[] = qsprintf( $conn, '(commit.commitIdentifier LIKE %> AND '. 'LENGTH(commit.commitIdentifier) = 40)', $identifier); } if ($refs) { $repositories = ipull($refs, 'repository'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIdentifiers($repositories); $repos->execute(); $repos = $repos->getIdentifierMap(); foreach ($refs as $key => $ref) { $repo = idx($repos, $ref['repository']); if (!$repo) { continue; } if ($repo->isSVN()) { if (!ctype_digit((string)$ref['identifier'])) { continue; } $sql[] = qsprintf( $conn, '(commit.repositoryID = %d AND commit.commitIdentifier = %s)', $repo->getID(), // NOTE: Because the 'commitIdentifier' column is a string, MySQL // ignores the index if we hand it an integer. Hand it a string. // See T3377. (int)$ref['identifier']); } else { if (strlen($ref['identifier']) < $min_qualified) { continue; } $identifier = $ref['identifier']; if (strlen($identifier) == 40) { // MySQL seems to do slightly better with this version if the // clause, so issue it if we have a full commit hash. $sql[] = qsprintf( $conn, '(commit.repositoryID = %d AND commit.commitIdentifier = %s)', $repo->getID(), $identifier); } else { $sql[] = qsprintf( $conn, '(commit.repositoryID = %d AND commit.commitIdentifier LIKE %>)', $repo->getID(), $identifier); } } } } if (!$sql) { // If we discarded all possible identifiers (e.g., they all referenced // bogus repositories or were all too short), make sure the query finds // nothing. throw new PhabricatorEmptyQueryException( pht('No commit identifiers.')); } $where[] = '('.implode(' OR ', $sql).')'; } if ($this->auditIDs !== null) { $where[] = qsprintf( $conn, 'auditor.id IN (%Ld)', $this->auditIDs); } if ($this->auditorPHIDs !== null) { $where[] = qsprintf( $conn, 'auditor.auditorPHID IN (%Ls)', $this->auditorPHIDs); } if ($this->statuses !== null) { + $statuses = array(); + foreach ($this->statuses as $status) { + $object = PhabricatorAuditCommitStatusConstants::newForLegacyStatus( + $status); + $statuses[] = $object->getLegacyKey(); + } + $where[] = qsprintf( $conn, 'commit.auditStatus IN (%Ld)', - $this->statuses); + $statuses); } if ($this->packagePHIDs !== null) { $where[] = qsprintf( $conn, 'package.dst IN (%Ls)', $this->packagePHIDs); } if ($this->unreachable !== null) { if ($this->unreachable) { $where[] = qsprintf( $conn, '(commit.importStatus & %d) = %d', PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE, PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE); } else { $where[] = qsprintf( $conn, '(commit.importStatus & %d) = 0', PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE); } } return $where; } protected function didFilterResults(array $filtered) { if ($this->identifierMap) { foreach ($this->identifierMap as $name => $commit) { if (isset($filtered[$commit->getPHID()])) { unset($this->identifierMap[$name]); } } } } private function shouldJoinAuditor() { return ($this->auditIDs || $this->auditorPHIDs); } private function shouldJoinOwners() { return (bool)$this->packagePHIDs; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $join = parent::buildJoinClauseParts($conn); $audit_request = new PhabricatorRepositoryAuditRequest(); if ($this->shouldJoinAuditor()) { $join[] = qsprintf( $conn, 'JOIN %T auditor ON commit.phid = auditor.commitPHID', $audit_request->getTableName()); } if ($this->shouldJoinOwners()) { $join[] = qsprintf( $conn, 'JOIN %T package ON commit.phid = package.src AND package.type = %s', PhabricatorEdgeConfig::TABLE_NAME_EDGE, DiffusionCommitHasPackageEdgeType::EDGECONST); } return $join; } protected function shouldGroupQueryResultRows() { if ($this->shouldJoinAuditor()) { return true; } if ($this->shouldJoinOwners()) { return true; } return parent::shouldGroupQueryResultRows(); } public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'epoch' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'epoch', 'type' => 'int', 'reverse' => false, ), ); } protected function getPagingValueMap($cursor, array $keys) { $commit = $this->loadCursorObject($cursor); return array( 'id' => $commit->getID(), 'epoch' => $commit->getEpoch(), ); } public function getBuiltinOrders() { $parent = parent::getBuiltinOrders(); // Rename the default ID-based orders. $parent['importnew'] = array( 'name' => pht('Import Date (Newest First)'), ) + $parent['newest']; $parent['importold'] = array( 'name' => pht('Import Date (Oldest First)'), ) + $parent['oldest']; return array( 'newest' => array( 'vector' => array('epoch', 'id'), 'name' => pht('Commit Date (Newest First)'), ), 'oldest' => array( 'vector' => array('-epoch', '-id'), 'name' => pht('Commit Date (Oldest First)'), ), ) + $parent; } } diff --git a/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php b/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php index 70584ae243..25984c93e1 100644 --- a/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php +++ b/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php @@ -1,186 +1,184 @@ objects = $objects; $phids = $query->getEvaluatedParameter('responsiblePHIDs'); if (!$phids) { throw new Exception( pht( 'You can not bucket results by required action without '. 'specifying "Responsible Users".')); } $phids = array_fuse($phids); $groups = array(); $groups[] = $this->newGroup() ->setName(pht('Needs Attention')) ->setNoDataString(pht('None of your commits have active concerns.')) ->setObjects($this->filterConcernRaised($phids)); $groups[] = $this->newGroup() ->setName(pht('Needs Verification')) ->setNoDataString(pht('No commits are awaiting your verification.')) ->setObjects($this->filterNeedsVerification($phids)); $groups[] = $this->newGroup() ->setName(pht('Ready to Audit')) ->setNoDataString(pht('No commits are waiting for you to audit them.')) ->setObjects($this->filterShouldAudit($phids)); $groups[] = $this->newGroup() ->setName(pht('Waiting on Authors')) ->setNoDataString(pht('None of your audits are waiting on authors.')) ->setObjects($this->filterWaitingOnAuthors($phids)); $groups[] = $this->newGroup() ->setName(pht('Waiting on Auditors')) ->setNoDataString(pht('None of your commits are waiting on audit.')) ->setObjects($this->filterWaitingOnAuditors($phids)); // Because you can apply these buckets to queries which include revisions // that have been closed, add an "Other" bucket if we still have stuff // that didn't get filtered into any of the previous buckets. if ($this->objects) { $groups[] = $this->newGroup() ->setName(pht('Other Commits')) ->setObjects($this->objects); } return $groups; } private function filterConcernRaised(array $phids) { $results = array(); $objects = $this->objects; foreach ($objects as $key => $object) { if (empty($phids[$object->getAuthorPHID()])) { continue; } if (!$object->isAuditStatusConcernRaised()) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterNeedsVerification(array $phids) { $results = array(); $objects = $this->objects; $has_concern = array( PhabricatorAuditStatusConstants::CONCERNED, ); $has_concern = array_fuse($has_concern); foreach ($objects as $key => $object) { if (isset($phids[$object->getAuthorPHID()])) { continue; } if (!$object->isAuditStatusNeedsVerification()) { continue; } if (!$this->hasAuditorsWithStatus($object, $phids, $has_concern)) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterShouldAudit(array $phids) { $results = array(); $objects = $this->objects; $should_audit = array( PhabricatorAuditStatusConstants::AUDIT_REQUIRED, PhabricatorAuditStatusConstants::AUDIT_REQUESTED, ); $should_audit = array_fuse($should_audit); foreach ($objects as $key => $object) { if (isset($phids[$object->getAuthorPHID()])) { continue; } if (!$this->hasAuditorsWithStatus($object, $phids, $should_audit)) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterWaitingOnAuthors(array $phids) { $results = array(); $objects = $this->objects; foreach ($objects as $key => $object) { if (!$object->isAuditStatusConcernRaised()) { continue; } if (isset($phids[$object->getAuthorPHID()])) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterWaitingOnAuditors(array $phids) { $results = array(); $objects = $this->objects; - $status_waiting = array( - PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT, - PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION, - PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED, - ); - $status_waiting = array_fuse($status_waiting); - foreach ($objects as $key => $object) { - if (empty($status_waiting[$object->getAuditStatus()])) { + $any_waiting = + $object->isAuditStatusNeedsAudit() || + $object->isAuditStatusNeedsVerification() || + $object->isAuditStatusPartiallyAudited(); + + if (!$any_waiting) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } } diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php index 1caf12a82d..bdc4a2e475 100644 --- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php +++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php @@ -1,352 +1,353 @@ getViewer(); $package = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->needPaths(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(); } $field_list = PhabricatorCustomField::getObjectFields( $package, PhabricatorCustomField::ROLE_VIEW); $field_list ->setViewer($viewer) ->readFieldsFromStorage($package); $curtain = $this->buildCurtain($package); $details = $this->buildPackageDetailView($package, $field_list); if ($package->isArchived()) { $header_icon = 'fa-ban'; $header_name = pht('Archived'); $header_color = 'dark'; } else { $header_icon = 'fa-check'; $header_name = pht('Active'); $header_color = 'bluegrey'; } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($package->getName()) ->setStatus($header_icon, $header_color, $header_name) ->setPolicyObject($package) ->setHeaderIcon('fa-gift'); $commit_views = array(); $commit_uri = id(new PhutilURI('/diffusion/commit/')) ->setQueryParams( array( 'package' => $package->getPHID(), )); - $status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED; + $status_concern = + PhabricatorAuditCommitStatusConstants::MODERN_CONCERN_RAISED; $attention_commits = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withPackagePHIDs(array($package->getPHID())) ->withStatuses( array( $status_concern, )) ->needCommitData(true) ->needAuditRequests(true) ->setLimit(10) ->execute(); $view = id(new PhabricatorAuditListView()) ->setUser($viewer) ->setNoDataString(pht('This package has no open problem commits.')) ->setCommits($attention_commits); $commit_views[] = array( 'view' => $view, 'header' => pht('Needs Attention'), 'icon' => 'fa-warning', 'button' => id(new PHUIButtonView()) ->setTag('a') ->setHref($commit_uri->alter('status', $status_concern)) ->setIcon('fa-list-ul') ->setText(pht('View All')), ); $all_commits = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withPackagePHIDs(array($package->getPHID())) ->needCommitData(true) ->needAuditRequests(true) ->setLimit(25) ->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'), 'icon' => 'fa-code', 'button' => id(new PHUIButtonView()) ->setTag('a') ->setHref($commit_uri) ->setIcon('fa-list-ul') ->setText(pht('View All')), ); $commit_panels = array(); foreach ($commit_views as $commit_view) { $commit_panel = id(new PHUIObjectBoxView()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); $commit_header = id(new PHUIHeaderView()) ->setHeader($commit_view['header']) ->setHeaderIcon($commit_view['icon']); if (isset($commit_view['button'])) { $commit_header->addActionLink($commit_view['button']); } $commit_panel->setHeader($commit_header); $commit_panel->appendChild($commit_view['view']); $commit_panels[] = $commit_panel; } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($package->getMonogram()); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $package, new PhabricatorOwnersPackageTransactionQuery()); $timeline->setShouldTerminate(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $this->renderPathsTable($paths, $repositories), $commit_panels, $timeline, )) ->addPropertySection(pht('Details'), $details); return $this->newPage() ->setTitle($package->getName()) ->setCrumbs($crumbs) ->appendChild($view); } private function buildPackageDetailView( PhabricatorOwnersPackage $package, PhabricatorCustomFieldList $field_list) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $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); $dominion = $package->getDominion(); $dominion_map = PhabricatorOwnersPackage::getDominionOptionsMap(); $spec = idx($dominion_map, $dominion, array()); $name = idx($spec, 'short', $dominion); $view->addProperty(pht('Dominion'), $name); $auto = $package->getAutoReview(); $autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap(); $spec = idx($autoreview_map, $auto, array()); $name = idx($spec, 'name', $auto); $view->addProperty(pht('Auto Review'), $name); if ($package->getAuditingEnabled()) { $auditing = pht('Enabled'); } else { $auditing = pht('Disabled'); } $view->addProperty(pht('Auditing'), $auditing); $ignored = $package->getIgnoredPathAttributes(); $ignored = array_keys($ignored); if ($ignored) { $ignored = implode(', ', $ignored); } else { $ignored = phutil_tag('em', array(), pht('None')); } $view->addProperty(pht('Ignored Attributes'), $ignored); $description = $package->getDescription(); if (strlen($description)) { $description = new PHUIRemarkupView($viewer, $description); $view->addSectionHeader(pht('Description')); $view->addTextContent($description); } $field_list->appendFieldsToPropertyList( $package, $viewer, $view); return $view; } private function buildCurtain(PhabricatorOwnersPackage $package) { $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $package, PhabricatorPolicyCapability::CAN_EDIT); $id = $package->getID(); $edit_uri = $this->getApplicationURI("/edit/{$id}/"); $paths_uri = $this->getApplicationURI("/paths/{$id}/"); $curtain = $this->newCurtainView($package); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Package')) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($edit_uri)); if ($package->isArchived()) { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Activate Package')) ->setIcon('fa-check') ->setDisabled(!$can_edit) ->setWorkflow($can_edit) ->setHref($this->getApplicationURI("/archive/{$id}/"))); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Archive Package')) ->setIcon('fa-ban') ->setDisabled(!$can_edit) ->setWorkflow($can_edit) ->setHref($this->getApplicationURI("/archive/{$id}/"))); } $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Paths')) ->setIcon('fa-folder-open') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($paths_uri)); return $curtain; } 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 = $repo->generateURI( array( 'branch' => $repo->getDefaultBranch(), 'path' => $path->getPathDisplay(), 'action' => 'browse', )); $path_link = phutil_tag( 'a', array( 'href' => (string)$href, ), $path->getPathDisplay()); $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', )); if ($info) { $table->setNotice($info); } $header = id(new PHUIHeaderView()) ->setHeader(pht('Paths')) ->setHeaderIcon('fa-folder-open'); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); return $box; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index f56962d9f8..291c895231 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -1,886 +1,887 @@ repository = $repository; return $this; } public function getRepository($assert_attached = true) { if ($assert_attached) { return $this->assertAttached($this->repository); } return $this->repository; } public function isPartiallyImported($mask) { return (($mask & $this->getImportStatus()) == $mask); } public function isImported() { return $this->isPartiallyImported(self::IMPORTED_ALL); } public function isUnreachable() { return $this->isPartiallyImported(self::IMPORTED_UNREACHABLE); } public function writeImportStatusFlag($flag) { return $this->adjustImportStatusFlag($flag, true); } public function clearImportStatusFlag($flag) { return $this->adjustImportStatusFlag($flag, false); } private function adjustImportStatusFlag($flag, $set) { $conn_w = $this->establishConnection('w'); $table_name = $this->getTableName(); $id = $this->getID(); if ($set) { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', $table_name, $flag, $id); $this->setImportStatus($this->getImportStatus() | $flag); } else { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus & ~%d) WHERE id = %d', $table_name, $flag, $id); $this->setImportStatus($this->getImportStatus() & ~$flag); } return $this; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'commitIdentifier' => 'text40', 'mailKey' => 'bytes20', 'authorPHID' => 'phid?', 'authorIdentityPHID' => 'phid?', 'committerIdentityPHID' => 'phid?', 'auditStatus' => 'uint32', 'summary' => 'text255', 'importStatus' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'repositoryID' => array( 'columns' => array('repositoryID', 'importStatus'), ), 'authorPHID' => array( 'columns' => array('authorPHID', 'auditStatus', 'epoch'), ), 'repositoryID_2' => array( 'columns' => array('repositoryID', 'epoch'), ), 'key_commit_identity' => array( 'columns' => array('commitIdentifier', 'repositoryID'), 'unique' => true, ), 'key_epoch' => array( 'columns' => array('epoch'), ), 'key_author' => array( 'columns' => array('authorPHID', 'epoch'), ), ), self::CONFIG_NO_MUTATE => array( 'importStatus', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryCommitPHIDType::TYPECONST); } public function loadCommitData() { if (!$this->getID()) { return null; } return id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $this->getID()); } public function attachCommitData( PhabricatorRepositoryCommitData $data = null) { $this->commitData = $data; return $this; } public function getCommitData() { return $this->assertAttached($this->commitData); } public function attachAudits(array $audits) { assert_instances_of($audits, 'PhabricatorRepositoryAuditRequest'); $this->audits = $audits; return $this; } public function getAudits() { return $this->assertAttached($this->audits); } public function hasAttachedAudits() { return ($this->audits !== self::ATTACHABLE); } public function attachIdentities( PhabricatorRepositoryIdentity $author = null, PhabricatorRepositoryIdentity $committer = null) { $this->authorIdentity = $author; $this->committerIdentity = $committer; return $this; } public function getAuthorIdentity() { return $this->assertAttached($this->authorIdentity); } public function getCommitterIdentity() { return $this->assertAttached($this->committerIdentity); } public function loadAndAttachAuditAuthority( PhabricatorUser $viewer, $actor_phid = null) { if ($actor_phid === null) { $actor_phid = $viewer->getPHID(); } // TODO: This method is a little weird and sketchy, but worlds better than // what came before it. Eventually, this should probably live in a Query // class. // Figure out which requests the actor has authority over: these are user // requests where they are the auditor, and packages and projects they are // a member of. if (!$actor_phid) { $attach_key = $viewer->getCacheFragment(); $phids = array(); } else { $attach_key = $actor_phid; // At least currently, when modifying your own commits, you act only on // behalf of yourself, not your packages/projects -- the idea being that // you can't accept your own commits. This may change or depend on // config. $actor_is_author = ($actor_phid == $this->getAuthorPHID()); if ($actor_is_author) { $phids = array($actor_phid); } else { $phids = array(); $phids[$actor_phid] = true; $owned_packages = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withAuthorityPHIDs(array($actor_phid)) ->execute(); foreach ($owned_packages as $package) { $phids[$package->getPHID()] = true; } $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withMemberPHIDs(array($actor_phid)) ->execute(); foreach ($projects as $project) { $phids[$project->getPHID()] = true; } $phids = array_keys($phids); } } $this->auditAuthorityPHIDs[$attach_key] = array_fuse($phids); return $this; } public function hasAuditAuthority( PhabricatorUser $viewer, PhabricatorRepositoryAuditRequest $audit, $actor_phid = null) { if ($actor_phid === null) { $actor_phid = $viewer->getPHID(); } if (!$actor_phid) { $attach_key = $viewer->getCacheFragment(); } else { $attach_key = $actor_phid; } $map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $attach_key); if (!$actor_phid) { return false; } return isset($map[$audit->getAuditorPHID()]); } public function writeOwnersEdges(array $package_phids) { $src_phid = $this->getPHID(); $edge_type = DiffusionCommitHasPackageEdgeType::EDGECONST; $editor = new PhabricatorEdgeEditor(); $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $src_phid, $edge_type); foreach ($dst_phids as $dst_phid) { $editor->removeEdge($src_phid, $edge_type, $dst_phid); } foreach ($package_phids as $package_phid) { $editor->addEdge($src_phid, $edge_type, $package_phid); } $editor->save(); return $this; } public function getAuditorPHIDsForEdit() { $audits = $this->getAudits(); return mpull($audits, 'getAuditorPHID'); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function delete() { $data = $this->loadCommitData(); $audits = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere('commitPHID = %s', $this->getPHID()); $this->openTransaction(); if ($data) { $data->delete(); } foreach ($audits as $audit) { $audit->delete(); } $result = parent::delete(); $this->saveTransaction(); return $result; } public function getDateCreated() { // This is primarily to make analysis of commits with the Fact engine work. return $this->getEpoch(); } public function getURI() { return '/'.$this->getMonogram(); } /** * Synchronize a commit's overall audit status with the individual audit * triggers. */ public function updateAuditStatus(array $requests) { assert_instances_of($requests, 'PhabricatorRepositoryAuditRequest'); $any_concern = false; $any_accept = false; $any_need = false; foreach ($requests as $request) { switch ($request->getAuditStatus()) { case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: $any_need = true; break; case PhabricatorAuditStatusConstants::ACCEPTED: $any_accept = true; break; case PhabricatorAuditStatusConstants::CONCERNED: $any_concern = true; break; } } - $current_status = $this->getAuditStatus(); - $status_verify = PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION; - if ($any_concern) { - if ($current_status == $status_verify) { + if ($this->isAuditStatusNeedsVerification()) { // If the change is in "Needs Verification", we keep it there as // long as any auditors still have concerns. - $status = $status_verify; + $status = PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION; } else { $status = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED; } } else if ($any_accept) { if ($any_need) { $status = PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED; } else { $status = PhabricatorAuditCommitStatusConstants::FULLY_AUDITED; } } else if ($any_need) { $status = PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT; } else { $status = PhabricatorAuditCommitStatusConstants::NONE; } return $this->setAuditStatus($status); } public function getMonogram() { $repository = $this->getRepository(); $callsign = $repository->getCallsign(); $identifier = $this->getCommitIdentifier(); if ($callsign !== null) { return "r{$callsign}{$identifier}"; } else { $id = $repository->getID(); return "R{$id}:{$identifier}"; } } public function getDisplayName() { $repository = $this->getRepository(); $identifier = $this->getCommitIdentifier(); return $repository->formatCommitName($identifier); } /** * Return a local display name for use in the context of the containing * repository. * * In Git and Mercurial, this returns only a short hash, like "abcdef012345". * See @{method:getDisplayName} for a short name that always includes * repository context. * * @return string Short human-readable name for use inside a repository. */ public function getLocalName() { $repository = $this->getRepository(); $identifier = $this->getCommitIdentifier(); return $repository->formatCommitName($identifier, $local = true); } /** * Make a strong effort to find a way to render this commit's committer. * This currently attempts to use @{PhabricatorRepositoryIdentity}, and * falls back to examining the commit detail information. After we force * the migration to using identities, update this method to remove the * fallback. See T12164 for details. */ public function renderAnyCommitter(PhabricatorUser $viewer, $handles) { $committer = $this->renderCommitter($viewer, $handles); if ($committer) { return $committer; } return $this->renderAuthor($viewer, $handles); } public function renderCommitter(PhabricatorUser $viewer, $handles) { $committer_phid = $this->getCommitterDisplayPHID(); if ($committer_phid) { return $handles[$committer_phid]->renderLink(); } $data = $this->getCommitData(); $committer_name = $data->getCommitDetail('committer'); if (strlen($committer_name)) { return DiffusionView::renderName($committer_name); } return null; } public function renderAuthor(PhabricatorUser $viewer, $handles) { $author_phid = $this->getAuthorDisplayPHID(); if ($author_phid) { return $handles[$author_phid]->renderLink(); } $data = $this->getCommitData(); $author_name = $data->getAuthorName(); if (strlen($author_name)) { return DiffusionView::renderName($author_name); } return null; } public function loadIdentities(PhabricatorUser $viewer) { if ($this->authorIdentity !== self::ATTACHABLE) { return $this; } $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIDs(array($this->getID())) ->needIdentities(true) ->executeOne(); $author_identity = $commit->getAuthorIdentity(); $committer_identity = $commit->getCommitterIdentity(); return $this->attachIdentities($author_identity, $committer_identity); } public function hasCommitterIdentity() { return ($this->getCommitterIdentity() !== null); } public function hasAuthorIdentity() { return ($this->getAuthorIdentity() !== null); } public function getCommitterDisplayPHID() { if ($this->hasCommitterIdentity()) { return $this->getCommitterIdentity()->getIdentityDisplayPHID(); } $data = $this->getCommitData(); return $data->getCommitDetail('committerPHID'); } public function getAuthorDisplayPHID() { if ($this->hasAuthorIdentity()) { return $this->getAuthorIdentity()->getIdentityDisplayPHID(); } $data = $this->getCommitData(); return $data->getCommitDetail('authorPHID'); } public function getAuditStatusObject() { $status = $this->getAuditStatus(); return PhabricatorAuditCommitStatusConstants::newForLegacyStatus($status); } public function isAuditStatusNoAudit() { return $this->getAuditStatusObject()->isNoAudit(); } + public function isAuditStatusNeedsAudit() { + return $this->getAuditStatusObject()->isNeedsAudit(); + } + public function isAuditStatusConcernRaised() { return $this->getAuditStatusObject()->isConcernRaised(); } public function isAuditStatusNeedsVerification() { return $this->getAuditStatusObject()->isNeedsVerification(); } public function isAuditStatusPartiallyAudited() { return $this->getAuditStatusObject()->isPartiallyAudited(); } public function isAuditStatusAudited() { return $this->getAuditStatusObject()->isAudited(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getRepository()->getPolicy($capability); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_USER; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Commits inherit the policies of the repository they belong to.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( Stuff for serialization )---------------------------------------------- */ /** * NOTE: this is not a complete serialization; only the 'protected' fields are * involved. This is due to ease of (ab)using the Lisk abstraction to get this * done, as well as complexity of the other fields. */ public function toDictionary() { return array( 'repositoryID' => $this->getRepositoryID(), 'phid' => $this->getPHID(), 'commitIdentifier' => $this->getCommitIdentifier(), 'epoch' => $this->getEpoch(), 'mailKey' => $this->getMailKey(), 'authorPHID' => $this->getAuthorPHID(), 'auditStatus' => $this->getAuditStatus(), 'summary' => $this->getSummary(), 'importStatus' => $this->getImportStatus(), ); } public static function newFromDictionary(array $dict) { return id(new PhabricatorRepositoryCommit()) ->loadFromArray($dict); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getHarbormasterBuildablePHID(); } public function getHarbormasterBuildablePHID() { return $this->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getRepository()->getPHID(); } public function getBuildVariables() { $results = array(); $results['buildable.commit'] = $this->getCommitIdentifier(); $repo = $this->getRepository(); $results['repository.callsign'] = $repo->getCallsign(); $results['repository.phid'] = $repo->getPHID(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); return $results; } public function getAvailableBuildVariables() { return array( 'buildable.commit' => pht('The commit identifier, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), 'repository.phid' => pht('The PHID of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => pht('The URI to clone or checkout the repository from.'), ); } public function newBuildableEngine() { return new DiffusionBuildableEngine(); } /* -( HarbormasterCircleCIBuildableInterface )----------------------------- */ public function getCircleCIGitHubRepositoryURI() { $repository = $this->getRepository(); $commit_phid = $this->getPHID(); $repository_phid = $repository->getPHID(); if ($repository->isHosted()) { throw new Exception( pht( 'This commit ("%s") is associated with a hosted repository '. '("%s"). Repositories must be imported from GitHub to be built '. 'with CircleCI.', $commit_phid, $repository_phid)); } $remote_uri = $repository->getRemoteURI(); $path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath( $remote_uri); if (!$path) { throw new Exception( pht( 'This commit ("%s") is associated with a repository ("%s") that '. 'with a remote URI ("%s") that does not appear to be hosted on '. 'GitHub. Repositories must be hosted on GitHub to be built with '. 'CircleCI.', $commit_phid, $repository_phid, $remote_uri)); } return $remote_uri; } public function getCircleCIBuildIdentifierType() { return 'revision'; } public function getCircleCIBuildIdentifier() { return $this->getCommitIdentifier(); } /* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */ public function getBuildkiteBranch() { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = $this->getRepository(); $branches = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, DiffusionRequest::newFromDictionary( array( 'repository' => $repository, 'user' => $viewer, )), 'diffusion.branchquery', array( 'contains' => $this->getCommitIdentifier(), 'repository' => $repository->getPHID(), )); if (!$branches) { throw new Exception( pht( 'Commit "%s" is not an ancestor of any branch head, so it can not '. 'be built with Buildkite.', $this->getCommitIdentifier())); } $branch = head($branches); return 'refs/heads/'.$branch['shortName']; } public function getBuildkiteCommit() { return $this->getCommitIdentifier(); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('diffusion.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorCommitCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { // TODO: This should also list auditors, but handling that is a bit messy // right now because we are not guaranteed to have the data. (It should not // include resigned auditors.) return ($phid == $this->getAuthorPHID()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorAuditEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorAuditTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { $xactions = $timeline->getTransactions(); $path_ids = array(); foreach ($xactions as $xaction) { if ($xaction->hasComment()) { $path_id = $xaction->getComment()->getPathID(); if ($path_id) { $path_ids[] = $path_id; } } } $path_map = array(); if ($path_ids) { $path_map = id(new DiffusionPathQuery()) ->withPathIDs($path_ids) ->execute(); $path_map = ipull($path_map, 'path', 'id'); } return $timeline->setPathMap($path_map); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DiffusionCommitFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new DiffusionCommitFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('identifier') ->setType('string') ->setDescription(pht('The commit identifier.')), ); } public function getFieldValuesForConduit() { // NOTE: This data should be similar to the information returned about // commmits by "differential.diff.search" with the "commits" attachment. return array( 'identifier' => $this->getCommitIdentifier(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorDraftInterface )------------------------------------------ */ public function newDraftEngine() { return new DiffusionCommitDraftEngine(); } public function getHasDraft(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment()); } public function attachHasDraft(PhabricatorUser $viewer, $has_draft) { $this->drafts[$viewer->getCacheFragment()] = $has_draft; return $this; } }