diff --git a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php index e559862c83..50e4180074 100644 --- a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php +++ b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php @@ -1,140 +1,164 @@ $spec) { if (idx($spec, 'legacy') == $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 getName() { return idx($this->spec, 'name', pht('Unknown ("%s")', $this->key)); } + public function isNoAudit() { + return ($this->key == self::MODERN_NONE); + } + + 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/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index c04bb8185b..d5c22fba6b 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -1,895 +1,893 @@ rawPatch = $patch; return $this; } public function getRawPatch() { return $this->rawPatch; } public function getEditorApplicationClass() { return 'PhabricatorDiffusionApplication'; } public function getEditorObjectsDescription() { return pht('Audits'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_INLINESTATE; $types[] = PhabricatorAuditTransaction::TYPE_COMMIT; // TODO: These will get modernized eventually, but that can happen one // at a time later on. $types[] = PhabricatorAuditActionConstants::INLINE; return $types; } protected function expandTransactions( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_INLINESTATE: $this->didExpandInlineState = true; break; } } $this->oldAuditStatus = $object->getAuditStatus(); $object->loadAndAttachAuditAuthority( $this->getActor(), $this->getActingAsPHID()); return parent::expandTransactions($object, $xactions); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::INLINE: return $xaction->hasComment(); } return parent::transactionHasEffect($object, $xaction); } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditTransaction::TYPE_COMMIT: return null; } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditTransaction::TYPE_COMMIT: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditTransaction::TYPE_COMMIT: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: return; case PhabricatorAuditActionConstants::INLINE: $reply = $xaction->getComment()->getReplyToComment(); if ($reply && !$reply->getHasReplies()) { $reply->setHasReplies(1)->save(); } return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyBuiltinExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_INLINESTATE: $table = new PhabricatorAuditTransactionComment(); $conn_w = $table->establishConnection('w'); foreach ($xaction->getNewValue() as $phid => $state) { queryfx( $conn_w, 'UPDATE %T SET fixedState = %s WHERE phid = %s', $table->getTableName(), $state, $phid); } break; } return parent::applyBuiltinExternalTransaction($object, $xaction); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // Load auditors explicitly; we may not have them if the caller was a // generic piece of infrastructure. $commit = id(new DiffusionCommitQuery()) ->setViewer($this->requireActor()) ->withIDs(array($object->getID())) ->needAuditRequests(true) ->executeOne(); if (!$commit) { throw new Exception( pht('Failed to load commit during transaction finalization!')); } $object->attachAudits($commit->getAudits()); $status_concerned = PhabricatorAuditStatusConstants::CONCERNED; $status_closed = PhabricatorAuditStatusConstants::CLOSED; $status_resigned = PhabricatorAuditStatusConstants::RESIGNED; $status_accepted = PhabricatorAuditStatusConstants::ACCEPTED; $status_concerned = PhabricatorAuditStatusConstants::CONCERNED; $actor_phid = $this->getActingAsPHID(); $actor_is_author = ($object->getAuthorPHID()) && ($actor_phid == $object->getAuthorPHID()); $import_status_flag = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $import_status_flag = PhabricatorRepositoryCommit::IMPORTED_HERALD; break; } } $old_status = $this->oldAuditStatus; $requests = $object->getAudits(); $object->updateAuditStatus($requests); $new_status = $object->getAuditStatus(); $object->save(); if ($import_status_flag) { $object->writeImportStatusFlag($import_status_flag); } - $partial_status = PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED; - // If the commit has changed state after this edit, add an informational // transaction about the state change. if ($old_status != $new_status) { - if ($new_status == $partial_status) { + if ($object->isAuditStatusPartiallyAudited()) { // This state isn't interesting enough to get a transaction. The // best way we could lead the user forward is something like "This // commit still requires additional audits." but that's redundant and // probably not very useful. } else { $xaction = $object->getApplicationTransactionTemplate() ->setTransactionType(DiffusionCommitStateTransaction::TRANSACTIONTYPE) ->setOldValue($old_status) ->setNewValue($new_status); $xaction = $this->populateTransaction($object, $xaction); $xaction->save(); } } // Collect auditor PHIDs for building mail. $this->auditorPHIDs = mpull($object->getAudits(), 'getAuditorPHID'); return $xactions; } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $xactions = parent::expandTransaction($object, $xaction); switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $request = $this->createAuditRequestTransactionFromCommitMessage( $object); if ($request) { $xactions[] = $request; $this->setUnmentionablePHIDMap($request->getNewValue()); } break; default: break; } if (!$this->didExpandInlineState) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $this->didExpandInlineState = true; $actor_phid = $this->getActingAsPHID(); $author_phid = $object->getAuthorPHID(); $actor_is_author = ($actor_phid == $author_phid); $state_map = PhabricatorTransactions::getInlineStateMap(); $query = id(new DiffusionDiffInlineCommentQuery()) ->setViewer($this->getActor()) ->withCommitPHIDs(array($object->getPHID())) ->withFixedStates(array_keys($state_map)); $inlines = array(); $inlines[] = id(clone $query) ->withAuthorPHIDs(array($actor_phid)) ->withHasTransaction(false) ->execute(); if ($actor_is_author) { $inlines[] = id(clone $query) ->withHasTransaciton(true) ->execute(); } $inlines = array_mergev($inlines); if (!$inlines) { break; } $old_value = mpull($inlines, 'getFixedState', 'getPHID'); $new_value = array(); foreach ($old_value as $key => $state) { $new_value[$key] = $state_map[$state]; } $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) ->setIgnoreOnNoEffect(true) ->setOldValue($old_value) ->setNewValue($new_value); break; } } return $xactions; } private function createAuditRequestTransactionFromCommitMessage( PhabricatorRepositoryCommit $commit) { $actor = $this->getActor(); $data = $commit->getCommitData(); $message = $data->getCommitMessage(); $result = DifferentialCommitMessageParser::newStandardParser($actor) ->setRaiseMissingFieldErrors(false) ->parseFields($message); $field_key = DifferentialAuditorsCommitMessageField::FIELDKEY; $phids = idx($result, $field_key, null); if (!$phids) { return array(); } // If a commit lists its author as an auditor, just pretend it does not. foreach ($phids as $key => $phid) { if ($phid == $commit->getAuthorPHID()) { unset($phids[$key]); } } if (!$phids) { return array(); } return $commit->getApplicationTransactionTemplate() ->setTransactionType(DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE) ->setNewValue( array( '+' => array_fuse($phids), )); } protected function sortTransactions(array $xactions) { $xactions = parent::sortTransactions($xactions); $head = array(); $tail = array(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == PhabricatorAuditActionConstants::INLINE) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function supportsSearch() { return true; } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, array $changes, PhutilMarkupEngine $engine) { $actor = $this->getActor(); $result = array(); // Some interactions (like "Fixes Txxx" interacting with Maniphest) have // already been processed, so we're only re-parsing them here to avoid // generating an extra redundant mention. Other interactions are being // processed for the first time. // We're only recognizing magic in the commit message itself, not in // audit comments. $is_commit = false; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $is_commit = true; break; } } if (!$is_commit) { return $result; } $flat_blocks = mpull($changes, 'getNewValue'); $huge_block = implode("\n\n", $flat_blocks); $phid_map = array(); $phid_map[] = $this->getUnmentionablePHIDMap(); $monograms = array(); $task_refs = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($huge_block); foreach ($task_refs as $match) { foreach ($match['monograms'] as $monogram) { $monograms[] = $monogram; } } $rev_refs = id(new DifferentialCustomFieldDependsOnParser()) ->parseCorpus($huge_block); foreach ($rev_refs as $match) { foreach ($match['monograms'] as $monogram) { $monograms[] = $monogram; } } $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withNames($monograms) ->execute(); $phid_map[] = mpull($objects, 'getPHID', 'getPHID'); $reverts_refs = id(new DifferentialCustomFieldRevertsParser()) ->parseCorpus($huge_block); $reverts = array_mergev(ipull($reverts_refs, 'monograms')); if ($reverts) { // Only allow commits to revert other commits in the same repository. $reverted_commits = id(new DiffusionCommitQuery()) ->setViewer($actor) ->withRepository($object->getRepository()) ->withIdentifiers($reverts) ->execute(); $reverted_revisions = id(new PhabricatorObjectQuery()) ->setViewer($actor) ->withNames($reverts) ->withTypes( array( DifferentialRevisionPHIDType::TYPECONST, )) ->execute(); $reverted_phids = mpull($reverted_commits, 'getPHID', 'getPHID') + mpull($reverted_revisions, 'getPHID', 'getPHID'); // NOTE: Skip any write attempts if a user cleverly implies a commit // reverts itself, although this would be exceptionally clever in Git // or Mercurial. unset($reverted_phids[$object->getPHID()]); $reverts_edge = DiffusionCommitRevertsCommitEdgeType::EDGECONST; $result[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $reverts_edge) ->setNewValue(array('+' => $reverted_phids)); $phid_map[] = $reverted_phids; } $phid_map = array_mergev($phid_map); $this->setUnmentionablePHIDMap($phid_map); return $result; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { $reply_handler = new PhabricatorAuditReplyHandler(); $reply_handler->setMailReceiver($object); return $reply_handler; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { // For backward compatibility, use this legacy thread ID. return 'diffusion-audit-'.$object->getPHID(); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $identifier = $object->getCommitIdentifier(); $repository = $object->getRepository(); $summary = $object->getSummary(); $name = $repository->formatCommitName($identifier); $subject = "{$name}: {$summary}"; $template = id(new PhabricatorMetaMTAMail()) ->setSubject($subject); $this->attachPatch( $template, $object); return $template; } protected function getMailTo(PhabricatorLiskDAO $object) { $this->requireAuditors($object); $phids = array(); if ($object->getAuthorPHID()) { $phids[] = $object->getAuthorPHID(); } foreach ($object->getAudits() as $audit) { if (!$audit->isInteresting()) { // Don't send mail to uninteresting auditors, like packages which // own this code but which audits have not triggered for. continue; } if (!$audit->isResigned()) { $phids[] = $audit->getAuditorPHID(); } } $phids[] = $this->getActingAsPHID(); return $phids; } protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) { $this->requireAuditors($object); $phids = array(); foreach ($object->getAudits() as $auditor) { if ($auditor->isResigned()) { $phids[] = $auditor->getAuditorPHID(); } } return $phids; } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $type_inline = PhabricatorAuditActionConstants::INLINE; $type_push = PhabricatorAuditTransaction::TYPE_COMMIT; $is_commit = false; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } if ($xaction->getTransactionType() == $type_push) { $is_commit = true; } } if ($inlines) { $body->addTextSection( pht('INLINE COMMENTS'), $this->renderInlineCommentsForMail($object, $inlines)); } if ($is_commit) { $data = $object->getCommitData(); $body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles); $this->inlinePatch( $body, $object); } $data = $object->getCommitData(); $user_phids = array(); $author_phid = $object->getAuthorPHID(); if ($author_phid) { $user_phids[$author_phid][] = pht('Author'); } $committer_phid = $data->getCommitDetail('committerPHID'); if ($committer_phid && ($committer_phid != $author_phid)) { $user_phids[$committer_phid][] = pht('Committer'); } foreach ($this->auditorPHIDs as $auditor_phid) { $user_phids[$auditor_phid][] = pht('Auditor'); } // TODO: It would be nice to show pusher here too, but that information // is a little tricky to get at right now. if ($user_phids) { $handle_phids = array_keys($user_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($handle_phids) ->execute(); $user_info = array(); foreach ($user_phids as $phid => $roles) { $user_info[] = pht( '%s (%s)', $handles[$phid]->getName(), implode(', ', $roles)); } $body->addTextSection( pht('USERS'), implode("\n", $user_info)); } $monogram = $object->getRepository()->formatCommitName( $object->getCommitIdentifier()); $body->addLinkSection( pht('COMMIT'), PhabricatorEnv::getProductionURI('/'.$monogram)); return $body; } private function attachPatch( PhabricatorMetaMTAMail $template, PhabricatorRepositoryCommit $commit) { if (!$this->getRawPatch()) { return; } $attach_key = 'metamta.diffusion.attach-patches'; $attach_patches = PhabricatorEnv::getEnvConfig($attach_key); if (!$attach_patches) { return; } $repository = $commit->getRepository(); $encoding = $repository->getDetail('encoding', 'UTF-8'); $raw_patch = $this->getRawPatch(); $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $template->addAttachment( new PhabricatorMetaMTAAttachment( $raw_patch, $commit_name.'.patch', 'text/x-patch; charset='.$encoding)); } private function inlinePatch( PhabricatorMetaMTAMailBody $body, PhabricatorRepositoryCommit $commit) { if (!$this->getRawPatch()) { return; } $inline_key = 'metamta.diffusion.inline-patches'; $inline_patches = PhabricatorEnv::getEnvConfig($inline_key); if (!$inline_patches) { return; } $repository = $commit->getRepository(); $raw_patch = $this->getRawPatch(); $result = null; $len = substr_count($raw_patch, "\n"); if ($len <= $inline_patches) { // We send email as utf8, so we need to convert the text to utf8 if // we can. $encoding = $repository->getDetail('encoding', 'UTF-8'); if ($encoding) { $raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding); } $result = phutil_utf8ize($raw_patch); } if ($result) { $result = "PATCH\n\n{$result}\n"; } $body->addRawSection($result); } private function renderInlineCommentsForMail( PhabricatorLiskDAO $object, array $inline_xactions) { $inlines = mpull($inline_xactions, 'getComment'); $block = array(); $path_map = id(new DiffusionPathQuery()) ->withPathIDs(mpull($inlines, 'getPathID')) ->execute(); $path_map = ipull($path_map, 'path', 'id'); foreach ($inlines as $inline) { $path = idx($path_map, $inline->getPathID()); if ($path === null) { continue; } $start = $inline->getLineNumber(); $len = $inline->getLineLength(); if ($len) { $range = $start.'-'.($start + $len); } else { $range = $start; } $content = $inline->getContent(); $block[] = "{$path}:{$range} {$content}"; } return implode("\n", $block); } public function getMailTagsMap() { return array( PhabricatorAuditTransaction::MAILTAG_COMMIT => pht('A commit is created.'), PhabricatorAuditTransaction::MAILTAG_ACTION_CONCERN => pht('A commit has a concerned raised against it.'), PhabricatorAuditTransaction::MAILTAG_ACTION_ACCEPT => pht('A commit is accepted.'), PhabricatorAuditTransaction::MAILTAG_ACTION_RESIGN => pht('A commit has an auditor resign.'), PhabricatorAuditTransaction::MAILTAG_ACTION_CLOSE => pht('A commit is closed.'), PhabricatorAuditTransaction::MAILTAG_ADD_AUDITORS => pht('A commit has auditors added.'), PhabricatorAuditTransaction::MAILTAG_ADD_CCS => pht("A commit's subscribers change."), PhabricatorAuditTransaction::MAILTAG_PROJECTS => pht("A commit's projects change."), PhabricatorAuditTransaction::MAILTAG_COMMENT => pht('Someone comments on a commit.'), PhabricatorAuditTransaction::MAILTAG_OTHER => pht('Other commit activity not listed above occurs.'), ); } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $repository = $object->getRepository(); if (!$repository->shouldPublish()) { return false; } return true; default: break; } } return parent::shouldApplyHeraldRules($object, $xactions); } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldCommitAdapter()) ->setObject($object); } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { $limit = self::MAX_FILES_SHOWN_IN_EMAIL; $files = $adapter->loadAffectedPaths(); sort($files); if (count($files) > $limit) { array_splice($files, $limit); $files[] = pht( '(This commit affected more than %d files. Only %d are shown here '. 'and additional ones are truncated.)', $limit, $limit); } $this->affectedFiles = implode("\n", $files); return array(); } private function isCommitMostlyImported(PhabricatorLiskDAO $object) { $has_message = PhabricatorRepositoryCommit::IMPORTED_MESSAGE; $has_changes = PhabricatorRepositoryCommit::IMPORTED_CHANGE; // Don't publish feed stories or email about events which occur during // import. In particular, this affects tasks being attached when they are // closed by "Fixes Txxxx" in a commit message. See T5851. $mask = ($has_message | $has_changes); return $object->isPartiallyImported($mask); } private function shouldPublishRepositoryActivity( PhabricatorLiskDAO $object, array $xactions) { // not every code path loads the repository so tread carefully // TODO: They should, and then we should simplify this. $repository = $object->getRepository($assert_attached = false); if ($repository != PhabricatorLiskDAO::ATTACHABLE) { if (!$repository->shouldPublish()) { return false; } } return $this->isCommitMostlyImported($object); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function shouldEnableMentions( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function getCustomWorkerState() { return array( 'rawPatch' => $this->rawPatch, 'affectedFiles' => $this->affectedFiles, 'auditorPHIDs' => $this->auditorPHIDs, ); } protected function getCustomWorkerStateEncoding() { return array( 'rawPatch' => self::STORAGE_ENCODING_BINARY, ); } protected function loadCustomWorkerState(array $state) { $this->rawPatch = idx($state, 'rawPatch'); $this->affectedFiles = idx($state, 'affectedFiles'); $this->auditorPHIDs = idx($state, 'auditorPHIDs'); return $this; } protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { return id(new DiffusionCommitQuery()) ->setViewer($this->requireActor()) ->withIDs(array($object->getID())) ->needAuditRequests(true) ->needCommitData(true) ->executeOne(); } private function requireAuditors(PhabricatorRepositoryCommit $commit) { if ($commit->hasAttachedAudits()) { return; } $with_auditors = id(new DiffusionCommitQuery()) ->setViewer($this->getActor()) ->needAuditRequests(true) ->withPHIDs(array($commit->getPHID())) ->executeOne(); if (!$with_auditors) { throw new Exception( pht( 'Failed to reload commit ("%s").', $commit->getPHID())); } $commit->attachAudits($with_auditors->getAudits()); } } diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index a6743b6557..283278fcf8 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -1,1155 +1,1154 @@ loadDiffusionContext(); if ($response) { return $response; } $drequest = $this->getDiffusionRequest(); $viewer = $request->getUser(); $repository = $drequest->getRepository(); $commit_identifier = $drequest->getCommit(); // If this page is being accessed via "/source/xyz/commit/...", redirect // to the canonical URI. $has_callsign = strlen($request->getURIData('repositoryCallsign')); $has_id = strlen($request->getURIData('repositoryID')); if (!$has_callsign && !$has_id) { $canonical_uri = $repository->getCommitURI($commit_identifier); return id(new AphrontRedirectResponse()) ->setURI($canonical_uri); } if ($request->getStr('diff')) { return $this->buildRawDiffResponse($drequest); } $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers(array($commit_identifier)) ->needCommitData(true) ->needAuditRequests(true) ->setLimit(100) ->needIdentities(true) ->execute(); $multiple_results = count($commits) > 1; $crumbs = $this->buildCrumbs(array( 'commit' => !$multiple_results, )); $crumbs->setBorder(true); if (!$commits) { if (!$this->getCommitExists()) { return new Aphront404Response(); } $error = id(new PHUIInfoView()) ->setTitle(pht('Commit Still Parsing')) ->appendChild( pht( 'Failed to load the commit because the commit has not been '. 'parsed yet.')); $title = pht('Commit Still Parsing'); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($error); } else if ($multiple_results) { $warning_message = pht( 'The identifier %s is ambiguous and matches more than one commit.', phutil_tag( 'strong', array(), $commit_identifier)); $error = id(new PHUIInfoView()) ->setTitle(pht('Ambiguous Commit')) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->appendChild($warning_message); $list = id(new DiffusionCommitListView()) ->setViewer($viewer) ->setCommits($commits) ->setNoDataString(pht('No recent commits.')); $crumbs->addTextCrumb(pht('Ambiguous Commit')); $matched_commits = id(new PHUITwoColumnView()) ->setFooter(array( $error, $list, )); return $this->newPage() ->setTitle(pht('Ambiguous Commit')) ->setCrumbs($crumbs) ->appendChild($matched_commits); } else { $commit = head($commits); } $audit_requests = $commit->getAudits(); $commit->loadAndAttachAuditAuthority($viewer); $commit_data = $commit->getCommitData(); $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub'); $error_panel = null; $hard_limit = 1000; if ($commit->isImported()) { $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $change_query->setLimit($hard_limit + 1); $changes = $change_query->loadChanges(); } else { $changes = array(); } $was_limited = (count($changes) > $hard_limit); if ($was_limited) { $changes = array_slice($changes, 0, $hard_limit); } $count = count($changes); $is_unreadable = false; $hint = null; if (!$count || $commit->isUnreachable()) { $hint = id(new DiffusionCommitHintQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withOldCommitIdentifiers(array($commit->getCommitIdentifier())) ->executeOne(); if ($hint) { $is_unreadable = $hint->isUnreadable(); } } if ($is_foreign) { $subpath = $commit_data->getCommitDetail('svn-subpath'); $error_panel = new PHUIInfoView(); $error_panel->setTitle(pht('Commit Not Tracked')); $error_panel->setSeverity(PHUIInfoView::SEVERITY_WARNING); $error_panel->appendChild( pht( "This Diffusion repository is configured to track only one ". "subdirectory of the entire Subversion repository, and this commit ". "didn't affect the tracked subdirectory ('%s'), so no ". "information is available.", $subpath)); } else { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $viewer); $commit_tag = $this->renderCommitHashTag($drequest); $header = id(new PHUIHeaderView()) ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))) ->setHeaderIcon('fa-code-fork') ->addTag($commit_tag); - if ($commit->getAuditStatus()) { - $icon = PhabricatorAuditCommitStatusConstants::getStatusIcon( - $commit->getAuditStatus()); - $color = PhabricatorAuditCommitStatusConstants::getStatusColor( - $commit->getAuditStatus()); - $status = PhabricatorAuditCommitStatusConstants::getStatusName( - $commit->getAuditStatus()); + if (!$commit->isAuditStatusNoAudit()) { + $status = $commit->getAuditStatusObject(); + + $icon = $status->getIcon(); + $color = $status->getColor(); + $status = $status->getName(); $header->setStatus($icon, $color, $status); } $curtain = $this->buildCurtain($commit, $repository); $subheader = $this->buildSubheaderView($commit, $commit_data); $details = $this->buildPropertyListView( $commit, $commit_data, $audit_requests); $message = $commit_data->getCommitMessage(); $revision = $commit->getCommitIdentifier(); $message = $this->linkBugtraq($message); $message = $engine->markupText($message); $detail_list = new PHUIPropertyListView(); $detail_list->addTextContent( phutil_tag( 'div', array( 'class' => 'diffusion-commit-message phabricator-remarkup', ), $message)); if ($commit->isUnreachable()) { $did_rewrite = false; if ($hint) { if ($hint->isRewritten()) { $rewritten = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers(array($hint->getNewCommitIdentifier())) ->executeOne(); if ($rewritten) { $did_rewrite = true; $rewritten_uri = $rewritten->getURI(); $rewritten_name = $rewritten->getLocalName(); $rewritten_link = phutil_tag( 'a', array( 'href' => $rewritten_uri, ), $rewritten_name); $this->commitErrors[] = pht( 'This commit was rewritten after it was published, which '. 'changed the commit hash. This old version of the commit is '. 'no longer reachable from any branch, tag or ref. The new '. 'version of this commit is %s.', $rewritten_link); } } } if (!$did_rewrite) { $this->commitErrors[] = pht( 'This commit has been deleted in the repository: it is no longer '. 'reachable from any branch, tag, or ref.'); } } if ($this->getCommitErrors()) { $error_panel = id(new PHUIInfoView()) ->appendChild($this->getCommitErrors()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING); } } $timeline = $this->buildComments($commit); $merge_table = $this->buildMergesTable($commit); $show_changesets = false; $info_panel = null; $change_list = null; $change_table = null; if ($is_unreadable) { $info_panel = $this->renderStatusMessage( pht('Unreadable Commit'), pht( 'This commit has been marked as unreadable by an administrator. '. 'It may have been corrupted or created improperly by an external '. 'tool.')); } else if ($is_foreign) { // Don't render anything else. } else if (!$commit->isImported()) { $info_panel = $this->renderStatusMessage( pht('Still Importing...'), pht( 'This commit is still importing. Changes will be visible once '. 'the import finishes.')); } else if (!count($changes)) { $info_panel = $this->renderStatusMessage( pht('Empty Commit'), pht( 'This commit is empty and does not affect any paths.')); } else if ($was_limited) { $info_panel = $this->renderStatusMessage( pht('Very Large Commit'), pht( 'This commit is very large, and affects more than %d files. '. 'Changes are not shown.', $hard_limit)); } else if (!$this->getCommitExists()) { $info_panel = $this->renderStatusMessage( pht('Commit No Longer Exists'), pht('This commit no longer exists in the repository.')); } else { $show_changesets = true; // The user has clicked "Show All Changes", and we should show all the // changes inline even if there are more than the soft limit. $show_all_details = $request->getBool('show_all'); $change_header = id(new PHUIHeaderView()) ->setHeader(pht('Changes (%s)', new PhutilNumber($count))); $warning_view = null; if ($count > self::CHANGES_LIMIT && !$show_all_details) { $button = id(new PHUIButtonView()) ->setText(pht('Show All Changes')) ->setHref('?show_all=true') ->setTag('a') ->setIcon('fa-files-o'); $warning_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setTitle(pht('Very Large Commit')) ->appendChild( pht('This commit is very large. Load each file individually.')); $change_header->addActionLink($button); } $changesets = DiffusionPathChange::convertToDifferentialChangesets( $viewer, $changes); // TODO: This table and panel shouldn't really be separate, but we need // to clean up the "Load All Files" interaction first. $change_table = $this->buildTableOfContents( $changesets, $change_header, $warning_view); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $vcs_supports_directory_changes = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $vcs_supports_directory_changes = false; break; default: throw new Exception(pht('Unknown VCS.')); } $references = array(); foreach ($changesets as $key => $changeset) { $file_type = $changeset->getFileType(); if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { if (!$vcs_supports_directory_changes) { unset($changesets[$key]); continue; } } $references[$key] = $drequest->generateURI( array( 'action' => 'rendering-ref', 'path' => $changeset->getFilename(), )); } // TODO: Some parts of the views still rely on properties of the // DifferentialChangeset. Make the objects ephemeral to make sure we don't // accidentally save them, and then set their ID to the appropriate ID for // this application (the path IDs). $path_ids = array_flip(mpull($changes, 'getPath')); foreach ($changesets as $changeset) { $changeset->makeEphemeral(); $changeset->setID($path_ids[$changeset->getFilename()]); } if ($count <= self::CHANGES_LIMIT || $show_all_details) { $visible_changesets = $changesets; } else { $visible_changesets = array(); $inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments( $viewer, $commit->getPHID()); $path_ids = mpull($inlines, null, 'getPathID'); foreach ($changesets as $key => $changeset) { if (array_key_exists($changeset->getID(), $path_ids)) { $visible_changesets[$key] = $changeset; } } } $change_list_title = $commit->getDisplayName(); $change_list = new DifferentialChangesetListView(); $change_list->setTitle($change_list_title); $change_list->setChangesets($changesets); $change_list->setVisibleChangesets($visible_changesets); $change_list->setRenderingReferences($references); $change_list->setRenderURI($repository->getPathURI('diff/')); $change_list->setRepository($repository); $change_list->setUser($viewer); $change_list->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); // TODO: Try to setBranch() to something reasonable here? $change_list->setStandaloneURI( $repository->getPathURI('diff/')); $change_list->setRawFileURIs( // TODO: Implement this, somewhat tricky if there's an octopus merge // or whatever? null, $repository->getPathURI('diff/?view=r')); $change_list->setInlineCommentControllerURI( '/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/'); } $add_comment = $this->renderAddCommentPanel( $commit, $timeline); $filetree_on = $viewer->compareUserSetting( PhabricatorShowFiletreeSetting::SETTINGKEY, PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE); $nav = null; if ($show_changesets && $filetree_on) { $pref_collapse = PhabricatorFiletreeVisibleSetting::SETTINGKEY; $collapsed = $viewer->getUserSetting($pref_collapse); $pref_width = PhabricatorFiletreeWidthSetting::SETTINGKEY; $width = $viewer->getUserSetting($pref_width); $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setTitle($commit->getDisplayName()) ->setBaseURI(new PhutilURI($commit->getURI())) ->build($changesets) ->setCrumbs($crumbs) ->setCollapsed((bool)$collapsed) ->setWidth((int)$width); } $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setMainColumn(array( $error_panel, $timeline, $merge_table, $info_panel, )) ->setFooter(array( $change_table, $change_list, $add_comment, )) ->addPropertySection(pht('Description'), $detail_list) ->addPropertySection(pht('Details'), $details) ->setCurtain($curtain); $page = $this->newPage() ->setTitle($commit->getDisplayName()) ->setCrumbs($crumbs) ->setPageObjectPHIDS(array($commit->getPHID())) ->appendChild( array( $view, )); if ($nav) { $page->setNavigation($nav); } return $page; } private function buildPropertyListView( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $audit_requests) { $viewer = $this->getViewer(); $commit_phid = $commit->getPHID(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $view = id(new PHUIPropertyListView()) ->setUser($this->getRequest()->getUser()) ->setObject($commit); $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($commit_phid)) ->withEdgeTypes(array( DiffusionCommitHasTaskEdgeType::EDGECONST, DiffusionCommitHasRevisionEdgeType::EDGECONST, DiffusionCommitRevertsCommitEdgeType::EDGECONST, DiffusionCommitRevertedByCommitEdgeType::EDGECONST, )); $edges = $edge_query->execute(); $task_phids = array_keys( $edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]); $revision_phid = key( $edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]); $reverts_phids = array_keys( $edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]); $reverted_by_phids = array_keys( $edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]); $phids = $edge_query->getDestinationPHIDs(array($commit_phid)); if ($data->getCommitDetail('reviewerPHID')) { $phids[] = $data->getCommitDetail('reviewerPHID'); } $phids[] = $commit->getCommitterDisplayPHID(); $phids[] = $commit->getAuthorDisplayPHID(); // NOTE: We should never normally have more than a single push log, but // it can occur naturally if a commit is pushed, then the branch it was // on is deleted, then the commit is pushed again (or through other similar // chains of events). This should be rare, but does not indicate a bug // or data issue. // NOTE: We never query push logs in SVN because the committer is always // the pusher and the commit time is always the push time; the push log // is redundant and we save a query by skipping it. $push_logs = array(); if ($repository->isHosted() && !$repository->isSVN()) { $push_logs = id(new PhabricatorRepositoryPushLogQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withNewRefs(array($commit->getCommitIdentifier())) ->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)) ->execute(); foreach ($push_logs as $log) { $phids[] = $log->getPusherPHID(); } } $handles = array(); if ($phids) { $handles = $this->loadViewerHandles($phids); } $props = array(); if ($audit_requests) { $user_requests = array(); $other_requests = array(); foreach ($audit_requests as $audit_request) { if (!$audit_request->isInteresting()) { continue; } if ($audit_request->isUser()) { $user_requests[] = $audit_request; } else { $other_requests[] = $audit_request; } } if ($user_requests) { $view->addProperty( pht('Auditors'), $this->renderAuditStatusView($commit, $user_requests)); } if ($other_requests) { $view->addProperty( pht('Group Auditors'), $this->renderAuditStatusView($commit, $other_requests)); } } $author_epoch = $data->getCommitDetail('authorEpoch'); $committed_info = id(new PHUIStatusItemView()) ->setNote(phabricator_datetime($commit->getEpoch(), $viewer)) ->setTarget($commit->renderAnyCommitter($viewer, $handles)); $committed_list = new PHUIStatusListView(); $committed_list->addItem($committed_info); $view->addProperty( pht('Committed'), $committed_list); if ($push_logs) { $pushed_list = new PHUIStatusListView(); foreach ($push_logs as $push_log) { $pushed_item = id(new PHUIStatusItemView()) ->setTarget($handles[$push_log->getPusherPHID()]->renderLink()) ->setNote(phabricator_datetime($push_log->getEpoch(), $viewer)); $pushed_list->addItem($pushed_item); } $view->addProperty( pht('Pushed'), $pushed_list); } $reviewer_phid = $data->getCommitDetail('reviewerPHID'); if ($reviewer_phid) { $view->addProperty( pht('Reviewer'), $handles[$reviewer_phid]->renderLink()); } if ($revision_phid) { $view->addProperty( pht('Differential Revision'), $handles[$revision_phid]->renderLink()); } $parents = $this->getCommitParents(); if ($parents) { $view->addProperty( pht('Parents'), $viewer->renderHandleList(mpull($parents, 'getPHID'))); } if ($this->getCommitExists()) { $view->addProperty( pht('Branches'), phutil_tag( 'span', array( 'id' => 'commit-branches', ), pht('Unknown'))); $view->addProperty( pht('Tags'), phutil_tag( 'span', array( 'id' => 'commit-tags', ), pht('Unknown'))); $identifier = $commit->getCommitIdentifier(); $root = $repository->getPathURI("commit/{$identifier}"); Javelin::initBehavior( 'diffusion-commit-branches', array( $root.'/branches/' => 'commit-branches', $root.'/tags/' => 'commit-tags', )); } $refs = $this->getCommitRefs(); if ($refs) { $ref_links = array(); foreach ($refs as $ref_data) { $ref_links[] = phutil_tag( 'a', array( 'href' => $ref_data['href'], ), $ref_data['ref']); } $view->addProperty( pht('References'), phutil_implode_html(', ', $ref_links)); } if ($reverts_phids) { $view->addProperty( pht('Reverts'), $viewer->renderHandleList($reverts_phids)); } if ($reverted_by_phids) { $view->addProperty( pht('Reverted By'), $viewer->renderHandleList($reverted_by_phids)); } if ($task_phids) { $task_list = array(); foreach ($task_phids as $phid) { $task_list[] = $handles[$phid]->renderLink(); } $task_list = phutil_implode_html(phutil_tag('br'), $task_list); $view->addProperty( pht('Tasks'), $task_list); } return $view; } private function buildSubheaderView( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); if ($repository->isSVN()) { return null; } $author_phid = $commit->getAuthorDisplayPHID(); $author_name = $data->getAuthorName(); $author_epoch = $data->getCommitDetail('authorEpoch'); $date = null; if ($author_epoch !== null) { $date = phabricator_datetime($author_epoch, $viewer); } if ($author_phid) { $handles = $viewer->loadHandles(array($author_phid)); $image_uri = $handles[$author_phid]->getImageURI(); $image_href = $handles[$author_phid]->getURI(); $author = $handles[$author_phid]->renderLink(); } else if (strlen($author_name)) { $author = $author_name; $image_uri = null; $image_href = null; } else { return null; } $author = phutil_tag('strong', array(), $author); if ($date) { $content = pht('Authored by %s on %s.', $author, $date); } else { $content = pht('Authored by %s.', $author); } return id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } private function buildComments(PhabricatorRepositoryCommit $commit) { $timeline = $this->buildTransactionTimeline( $commit, new PhabricatorAuditTransactionQuery()); $commit->willRenderTimeline($timeline, $this->getRequest()); $timeline->setQuoteRef($commit->getMonogram()); return $timeline; } private function renderAddCommentPanel( PhabricatorRepositoryCommit $commit, $timeline) { $request = $this->getRequest(); $viewer = $request->getUser(); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); $comment_view = id(new DiffusionCommitEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($commit); $comment_view->setTransactionTimeline($timeline); return $comment_view; } private function buildMergesTable(PhabricatorRepositoryCommit $commit) { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $merges = $this->getCommitMerges(); if (!$merges) { return null; } $limit = $this->getMergeDisplayLimit(); $caption = null; if (count($merges) > $limit) { $merges = array_slice($merges, 0, $limit); $caption = new PHUIInfoView(); $caption->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $caption->appendChild( pht( 'This commit merges a very large number of changes. '. 'Only the first %s are shown.', new PhutilNumber($limit))); } $history_table = id(new DiffusionHistoryTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setHistory($merges); $history_table->loadRevisions(); $panel = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Merged Changes')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($history_table); if ($caption) { $panel->setInfoView($caption); } return $panel; } private function buildCurtain( PhabricatorRepositoryCommit $commit, PhabricatorRepository $repository) { $request = $this->getRequest(); $viewer = $this->getViewer(); $curtain = $this->newCurtainView($commit); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $commit, PhabricatorPolicyCapability::CAN_EDIT); $id = $commit->getID(); $edit_uri = $this->getApplicationURI("/commit/edit/{$id}/"); $action = id(new PhabricatorActionView()) ->setName(pht('Edit Commit')) ->setHref($edit_uri) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); $curtain->addAction($action); $action = id(new PhabricatorActionView()) ->setName(pht('Download Raw Diff')) ->setHref($request->getRequestURI()->alter('diff', true)) ->setIcon('fa-download'); $curtain->addAction($action); $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $commit); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { $curtain->addAction($relationship_submenu); } return $curtain; } private function buildRawDiffResponse(DiffusionRequest $drequest) { $diff_info = $this->callConduitWithDiffusionRequest( 'diffusion.rawdiffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), )); $file_phid = $diff_info['filePHID']; $file = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file ("%s") returned by "%s".', $file_phid, 'diffusion.rawdiffquery')); } return $file->getRedirectResponse(); } private function renderAuditStatusView( PhabricatorRepositoryCommit $commit, array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $viewer = $this->getViewer(); $view = new PHUIStatusListView(); foreach ($audit_requests as $request) { $code = $request->getAuditStatus(); $item = new PHUIStatusItemView(); $item->setIcon( PhabricatorAuditStatusConstants::getStatusIcon($code), PhabricatorAuditStatusConstants::getStatusColor($code), PhabricatorAuditStatusConstants::getStatusName($code)); $auditor_phid = $request->getAuditorPHID(); $target = $viewer->renderHandle($auditor_phid); $item->setTarget($target); if ($commit->hasAuditAuthority($viewer, $request)) { $item->setHighlighted(true); } $view->addItem($item); } return $view; } private function linkBugtraq($corpus) { $url = PhabricatorEnv::getEnvConfig('bugtraq.url'); if (!strlen($url)) { return $corpus; } $regexes = PhabricatorEnv::getEnvConfig('bugtraq.logregex'); if (!$regexes) { return $corpus; } $parser = id(new PhutilBugtraqParser()) ->setBugtraqPattern("[[ {$url} | %BUGID% ]]") ->setBugtraqCaptureExpression(array_shift($regexes)); $select = array_shift($regexes); if ($select) { $parser->setBugtraqSelectExpression($select); } return $parser->processCorpus($corpus); } private function buildTableOfContents( array $changesets, $header, $info_view) { $drequest = $this->getDiffusionRequest(); $viewer = $this->getViewer(); $toc_view = id(new PHUIDiffTableOfContentsListView()) ->setUser($viewer) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); if ($info_view) { $toc_view->setInfoView($info_view); } // TODO: This is hacky, we just want access to the linkX() methods on // DiffusionView. $diffusion_view = id(new DiffusionEmptyResultView()) ->setDiffusionRequest($drequest); $have_owners = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorOwnersApplication', $viewer); if (!$changesets) { $have_owners = false; } if ($have_owners) { if ($viewer->getPHID()) { $packages = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withAuthorityPHIDs(array($viewer->getPHID())) ->execute(); $toc_view->setAuthorityPackages($packages); } $repository = $drequest->getRepository(); $repository_phid = $repository->getPHID(); $control_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withControl($repository_phid, mpull($changesets, 'getFilename')); $control_query->execute(); } foreach ($changesets as $changeset_id => $changeset) { $path = $changeset->getFilename(); $anchor = $changeset->getAnchorName(); $history_link = $diffusion_view->linkHistory($path); $browse_link = $diffusion_view->linkBrowse( $path, array( 'type' => $changeset->getFileType(), )); $item = id(new PHUIDiffTableOfContentsItemView()) ->setChangeset($changeset) ->setAnchor($anchor) ->setContext( array( $history_link, ' ', $browse_link, )); if ($have_owners) { $packages = $control_query->getControllingPackagesForPath( $repository_phid, $changeset->getFilename()); $item->setPackages($packages); } $toc_view->addItem($item); } return $toc_view; } private function loadCommitState() { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $commit = $drequest->getCommit(); // TODO: We could use futures here and resolve these calls in parallel. $exceptions = array(); try { $parent_refs = $this->callConduitWithDiffusionRequest( 'diffusion.commitparentsquery', array( 'commit' => $commit, )); if ($parent_refs) { $parents = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers($parent_refs) ->execute(); } else { $parents = array(); } $this->commitParents = $parents; } catch (Exception $ex) { $this->commitParents = false; $exceptions[] = $ex; } $merge_limit = $this->getMergeDisplayLimit(); try { if ($repository->isSVN()) { $this->commitMerges = array(); } else { $merges = $this->callConduitWithDiffusionRequest( 'diffusion.mergedcommitsquery', array( 'commit' => $commit, 'limit' => $merge_limit + 1, )); $this->commitMerges = DiffusionPathChange::newFromConduit($merges); } } catch (Exception $ex) { $this->commitMerges = false; $exceptions[] = $ex; } try { if ($repository->isGit()) { $refs = $this->callConduitWithDiffusionRequest( 'diffusion.refsquery', array( 'commit' => $commit, )); } else { $refs = array(); } $this->commitRefs = $refs; } catch (Exception $ex) { $this->commitRefs = false; $exceptions[] = $ex; } if ($exceptions) { $exists = $this->callConduitWithDiffusionRequest( 'diffusion.existsquery', array( 'commit' => $commit, )); if ($exists) { $this->commitExists = true; foreach ($exceptions as $exception) { $this->commitErrors[] = $exception->getMessage(); } } else { $this->commitExists = false; $this->commitErrors[] = pht( 'This commit no longer exists in the repository. It may have '. 'been part of a branch which was deleted.'); } } else { $this->commitExists = true; $this->commitErrors = array(); } } private function getMergeDisplayLimit() { return 50; } private function getCommitExists() { if ($this->commitExists === null) { $this->loadCommitState(); } return $this->commitExists; } private function getCommitParents() { if ($this->commitParents === null) { $this->loadCommitState(); } return $this->commitParents; } private function getCommitRefs() { if ($this->commitRefs === null) { $this->loadCommitState(); } return $this->commitRefs; } private function getCommitMerges() { if ($this->commitMerges === null) { $this->loadCommitState(); } return $this->commitMerges; } private function getCommitErrors() { if ($this->commitErrors === null) { $this->loadCommitState(); } return $this->commitErrors; } } diff --git a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php index 088f4b752e..712b7ae150 100644 --- a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php +++ b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php @@ -1,188 +1,179 @@ auditRequests; } public function canPublishStory(PhabricatorFeedStory $story, $object) { return ($story instanceof PhabricatorApplicationTransactionFeedStory) && ($object instanceof PhabricatorRepositoryCommit); } public function isStoryAboutObjectCreation($object) { // TODO: Although creation stories exist, they currently don't have a // primary object PHID set, so they'll never make it here because they // won't pass `canPublishStory()`. return false; } public function isStoryAboutObjectClosure($object) { // TODO: This isn't quite accurate, but pretty close: check if this story // is a close (which clearly is about object closure) or is an "Accept" and // the commit is fully audited (which is almost certainly a closure). // After ApplicationTransactions, we could annotate feed stories more // explicitly. - $fully_audited = PhabricatorAuditCommitStatusConstants::FULLY_AUDITED; - $story = $this->getFeedStory(); $xaction = $story->getPrimaryTransaction(); switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: switch ($xaction->getNewValue()) { case PhabricatorAuditActionConstants::CLOSE: return true; case PhabricatorAuditActionConstants::ACCEPT: - if ($object->getAuditStatus() == $fully_audited) { + if ($object->isAuditStatusAudited()) { return true; } break; } } return false; } public function willPublishStory($commit) { $requests = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($commit->getPHID())) ->needAuditRequests(true) ->executeOne() ->getAudits(); // TODO: This is messy and should be generalized, but we don't have a good // query for it yet. Since we run in the daemons, just do the easiest thing // we can for the moment. Figure out who all of the "active" (need to // audit) and "passive" (no action necessary) users are. $auditor_phids = mpull($requests, 'getAuditorPHID'); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($auditor_phids) ->execute(); $active = array(); $passive = array(); foreach ($requests as $request) { $status = $request->getAuditStatus(); $object = idx($objects, $request->getAuditorPHID()); if (!$object) { continue; } $request_phids = array(); if ($object instanceof PhabricatorUser) { $request_phids = array($object->getPHID()); } else if ($object instanceof PhabricatorOwnersPackage) { $request_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs( array($object->getID())); } else if ($object instanceof PhabricatorProject) { $project = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withIDs(array($object->getID())) ->needMembers(true) ->executeOne(); $request_phids = $project->getMemberPHIDs(); } else { // Dunno what this is. $request_phids = array(); } switch ($status) { case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: case PhabricatorAuditStatusConstants::CONCERNED: $active += array_fuse($request_phids); break; default: $passive += array_fuse($request_phids); break; } } // Remove "Active" users from the "Passive" list. $passive = array_diff_key($passive, $active); $this->activePHIDs = $active; $this->passivePHIDs = $passive; $this->auditRequests = $requests; return $commit; } public function getOwnerPHID($object) { return $object->getAuthorPHID(); } public function getActiveUserPHIDs($object) { return $this->activePHIDs; } public function getPassiveUserPHIDs($object) { return $this->passivePHIDs; } public function getCCUserPHIDs($object) { return PhabricatorSubscribersQuery::loadSubscribersForPHID( $object->getPHID()); } public function getObjectTitle($object) { $prefix = $this->getTitlePrefix($object); $repository = $object->getRepository(); $name = $repository->formatCommitName($object->getCommitIdentifier()); $title = $object->getSummary(); return ltrim("{$prefix} {$name}: {$title}"); } public function getObjectURI($object) { $repository = $object->getRepository(); $name = $repository->formatCommitName($object->getCommitIdentifier()); return PhabricatorEnv::getProductionURI('/'.$name); } public function getObjectDescription($object) { $data = $object->loadCommitData(); if ($data) { return $data->getCommitMessage(); } return null; } public function isObjectClosed($object) { - switch ($object->getAuditStatus()) { - case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT: - case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: - case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED: - return false; - default: - return true; - } + return $object->getAuditStatusObject()->getIsClosed(); } public function getResponsibilityTitle($object) { $prefix = $this->getTitlePrefix($object); return pht('%s Audit', $prefix); } private function getTitlePrefix(PhabricatorRepositoryCommit $commit) { $prefix_key = 'metamta.diffusion.subject-prefix'; return PhabricatorEnv::getEnvConfig($prefix_key); } } diff --git a/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php b/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php index 0711028796..3ce95cb3c3 100644 --- a/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php +++ b/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php @@ -1,53 +1,53 @@ getViewer(); $author_phid = $commit->getAuthorPHID(); if ($author_phid) { $author = $viewer->renderHandle($author_phid); } else { $commit_data = $commit->loadCommitData(); $author = phutil_tag('em', array(), $commit_data->getAuthorName()); } $hovercard->setTitle($handle->getName()); $hovercard->setDetail($commit->getSummary()); $hovercard->addField(pht('Author'), $author); $hovercard->addField(pht('Date'), phabricator_date($commit->getEpoch(), $viewer)); - if ($commit->getAuditStatus() != - PhabricatorAuditCommitStatusConstants::NONE) { + if (!$commit->isAuditStatusNoAudit()) { + $status = $commit->getAuditStatusObject(); - $hovercard->addField(pht('Audit Status'), - PhabricatorAuditCommitStatusConstants::getStatusName( - $commit->getAuditStatus())); + $hovercard->addField( + pht('Audit Status'), + $status->getName()); } } } diff --git a/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php b/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php index 40bc63025d..70584ae243 100644 --- a/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php +++ b/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php @@ -1,191 +1,186 @@ 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; - $status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED; - foreach ($objects as $key => $object) { if (empty($phids[$object->getAuthorPHID()])) { continue; } - if ($object->getAuditStatus() != $status_concern) { + if (!$object->isAuditStatusConcernRaised()) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterNeedsVerification(array $phids) { $results = array(); $objects = $this->objects; - $status_verify = PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION; $has_concern = array( PhabricatorAuditStatusConstants::CONCERNED, ); $has_concern = array_fuse($has_concern); foreach ($objects as $key => $object) { if (isset($phids[$object->getAuthorPHID()])) { continue; } - if ($object->getAuditStatus() != $status_verify) { + 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; - $status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED; - foreach ($objects as $key => $object) { - if ($object->getAuditStatus() != $status_concern) { + 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()])) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } } diff --git a/src/applications/diffusion/xaction/DiffusionCommitConcernTransaction.php b/src/applications/diffusion/xaction/DiffusionCommitConcernTransaction.php index 30ec8d9f6d..07194a2af3 100644 --- a/src/applications/diffusion/xaction/DiffusionCommitConcernTransaction.php +++ b/src/applications/diffusion/xaction/DiffusionCommitConcernTransaction.php @@ -1,82 +1,80 @@ setAuditStatus( PhabricatorAuditCommitStatusConstants::CONCERN_RAISED); } public function applyExternalEffects($object, $value) { $status = PhabricatorAuditStatusConstants::CONCERNED; $actor = $this->getActor(); $this->applyAuditorEffect($object, $actor, $value, $status); } protected function validateAction($object, PhabricatorUser $viewer) { if ($this->isViewerCommitAuthor($object, $viewer)) { throw new Exception( pht( 'You can not raise a concern with this commit because you are '. 'the commit author. You can only raise concerns with commits '. 'you did not author.')); } // Even if you've already raised a concern, you can raise again as long // as the author requested you verify. - $state_verify = PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION; - if ($this->isViewerFullyRejected($object, $viewer)) { - if ($object->getAuditStatus() != $state_verify) { + if (!$object->isAuditStatusNeedsVerification()) { throw new Exception( pht( 'You can not raise a concern with this commit because you have '. 'already raised a concern with it.')); } } } public function getTitle() { return pht( '%s raised a concern with this commit.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s raised a concern with %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/diffusion/xaction/DiffusionCommitStateTransaction.php b/src/applications/diffusion/xaction/DiffusionCommitStateTransaction.php index a6288923f4..bcf6a9e382 100644 --- a/src/applications/diffusion/xaction/DiffusionCommitStateTransaction.php +++ b/src/applications/diffusion/xaction/DiffusionCommitStateTransaction.php @@ -1,72 +1,75 @@ getNewValue(); - return PhabricatorAuditCommitStatusConstants::getStatusIcon($new); + return PhabricatorAuditCommitStatusConstants::newForLegacyStatus($new); + } + + public function getIcon() { + return $this->getAuditStatusObject()->getIcon(); } public function getColor() { - $new = $this->getNewValue(); - return PhabricatorAuditCommitStatusConstants::getStatusColor($new); + return $this->getAuditStatusObject()->getColor(); } public function getTitle() { - $new = $this->getNewValue(); + $status = $this->getAuditStatusObject(); - switch ($new) { - case PhabricatorAuditCommitStatusConstants::NONE: + switch ($status->getKey()) { + case PhabricatorAuditCommitStatusConstants::MODERN_NONE: return pht('This commit no longer requires audit.'); - case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT: + case PhabricatorAuditCommitStatusConstants::MODERN_NEEDS_AUDIT: return pht('This commit now requires audit.'); - case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: + case PhabricatorAuditCommitStatusConstants::MODERN_CONCERN_RAISED: return pht('This commit now has outstanding concerns.'); - case PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION: + case PhabricatorAuditCommitStatusConstants::MODERN_NEEDS_VERIFICATION: return pht('This commit now requires verification by auditors.'); - case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED: + case PhabricatorAuditCommitStatusConstants::MODERN_AUDITED: return pht('All concerns with this commit have now been addressed.'); } return null; } public function getTitleForFeed() { - $new = $this->getNewValue(); + $status = $this->getAuditStatusObject(); - switch ($new) { - case PhabricatorAuditCommitStatusConstants::NONE: + switch ($status->getKey()) { + case PhabricatorAuditCommitStatusConstants::MODERN_NONE: return pht( '%s no longer requires audit.', $this->renderObject()); - case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT: + case PhabricatorAuditCommitStatusConstants::MODERN_NEEDS_AUDIT: return pht( '%s now requires audit.', $this->renderObject()); - case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: + case PhabricatorAuditCommitStatusConstants::MODERN_CONCERN_RAISED: return pht( '%s now has outstanding concerns.', $this->renderObject()); - case PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION: + case PhabricatorAuditCommitStatusConstants::MODERN_NEEDS_VERIFICATION: return pht( '%s now requires verification by auditors.', $this->renderObject()); - case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED: + case PhabricatorAuditCommitStatusConstants::MODERN_AUDITED: return pht( 'All concerns with %s have now been addressed.', $this->renderObject()); } return null; } } diff --git a/src/applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php b/src/applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php index a44261f7a4..3e6abb705f 100644 --- a/src/applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php +++ b/src/applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php @@ -1,73 +1,72 @@ setAuditStatus( PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION); } protected function validateAction($object, PhabricatorUser $viewer) { if (!$this->isViewerCommitAuthor($object, $viewer)) { throw new Exception( pht( 'You can not request verification of this commit because you '. 'are not the author.')); } - $status = $object->getAuditStatus(); - if ($status != PhabricatorAuditCommitStatusConstants::CONCERN_RAISED) { + if (!$object->isAuditStatusConcernRaised()) { throw new Exception( pht( 'You can not request verification of this commit because no '. 'auditors have raised concerns with it.')); } } public function getTitle() { return pht( '%s requested verification of this commit.', $this->renderAuthor()); } public function getTitleForFeed() { return pht( '%s requested verification of %s.', $this->renderAuthor(), $this->renderObject()); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 447db08e6c..f56962d9f8 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -1,866 +1,886 @@ 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 the change is in "Needs Verification", we keep it there as // long as any auditors still have concerns. $status = $status_verify; } 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 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; } }