diff --git a/src/applications/differential/constants/DifferentialAction.php b/src/applications/differential/constants/DifferentialAction.php index 57038959de..9238990161 100644 --- a/src/applications/differential/constants/DifferentialAction.php +++ b/src/applications/differential/constants/DifferentialAction.php @@ -1,138 +1,138 @@ pht('Comment'), self::ACTION_ACCEPT => pht("Accept Revision \xE2\x9C\x94"), self::ACTION_REJECT => pht("Request Changes \xE2\x9C\x98"), self::ACTION_RETHINK => pht("Plan Changes \xE2\x9C\x98"), self::ACTION_ABANDON => pht('Abandon Revision'), self::ACTION_REQUEST => pht('Request Review'), self::ACTION_RECLAIM => pht('Reclaim Revision'), self::ACTION_RESIGN => pht('Resign as Reviewer'), self::ACTION_ADDREVIEWERS => pht('Add Reviewers'), self::ACTION_ADDCCS => pht('Add Subscribers'), self::ACTION_CLOSE => pht('Close Revision'), self::ACTION_CLAIM => pht('Commandeer Revision'), self::ACTION_REOPEN => pht('Reopen'), ); if (!empty($verbs[$action])) { return $verbs[$action]; } else { return 'brazenly '.$action; } } public static function allowReviewers($action) { if ($action == DifferentialAction::ACTION_ADDREVIEWERS || $action == DifferentialAction::ACTION_REQUEST || $action == DifferentialAction::ACTION_RESIGN) { return true; } return false; } } diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index 5b7fce871b..55c589b6e3 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -1,512 +1,600 @@ isCommandeerSideEffect = $is_side_effect; return $this; } public function getIsCommandeerSideEffect() { return $this->isCommandeerSideEffect; } const TYPE_INLINE = 'differential:inline'; const TYPE_UPDATE = 'differential:update'; const TYPE_ACTION = 'differential:action'; const TYPE_STATUS = 'differential:status'; public function getApplicationName() { return 'differential'; } public function getApplicationTransactionType() { return DifferentialRevisionPHIDType::TYPECONST; } public function getApplicationTransactionCommentObject() { return new DifferentialTransactionComment(); } public function shouldHide() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_UPDATE: // Older versions of this transaction have an ID for the new value, // and/or do not record the old value. Only hide the transaction if // the new value is a PHID, indicating that this is a newer style // transaction. if ($old === null) { if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) { return true; } } break; case PhabricatorTransactions::TYPE_EDGE: $add = array_diff_key($new, $old); $rem = array_diff_key($old, $new); // Hide metadata-only edge transactions. These correspond to users // accepting or rejecting revisions, but the change is always explicit // because of the TYPE_ACTION transaction. Rendering these transactions // just creates clutter. if (!$add && !$rem) { return true; } break; } return parent::shouldHide(); } public function shouldHideForMail(array $xactions) { switch ($this->getTransactionType()) { case self::TYPE_INLINE: // Hide inlines when rendering mail transactions if any other // transaction type exists. foreach ($xactions as $xaction) { if ($xaction->getTransactionType() != self::TYPE_INLINE) { return true; } } // If only inline transactions exist, we just render the first one. return ($this !== head($xactions)); } return parent::shouldHideForMail($xactions); } public function getBodyForMail() { switch ($this->getTransactionType()) { case self::TYPE_INLINE: // Don't render inlines into the mail body; they render into a special // section immediately after the body instead. return null; } return parent::getBodyForMail(); } public function getRequiredHandlePHIDs() { $phids = parent::getRequiredHandlePHIDs(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { + case self::TYPE_ACTION: + if ($new == DifferentialAction::ACTION_CLOSE && + $this->getMetadataValue('isCommitClose')) { + $phids[] = $this->getMetadataValue('commitPHID'); + if ($this->getMetadataValue('committerPHID')) { + $phids[] = $this->getMetadataValue('committerPHID'); + } + if ($this->getMetadataValue('authorPHID')) { + $phids[] = $this->getMetadataValue('authorPHID'); + } + } + break; case self::TYPE_UPDATE: if ($new) { $phids[] = $new; } break; } return $phids; } public function getActionStrength() { switch ($this->getTransactionType()) { case self::TYPE_ACTION: return 3; case self::TYPE_UPDATE: return 2; case self::TYPE_INLINE: return 0.25; } return parent::getActionStrength(); } public function getActionName() { switch ($this->getTransactionType()) { case self::TYPE_INLINE: return pht('Commented On'); case self::TYPE_UPDATE: $old = $this->getOldValue(); if ($old === null) { return pht('Request'); } else { return pht('Updated'); } case self::TYPE_ACTION: $map = array( DifferentialAction::ACTION_ACCEPT => pht('Accepted'), DifferentialAction::ACTION_REJECT => pht('Requested Changes To'), DifferentialAction::ACTION_RETHINK => pht('Planned Changes To'), DifferentialAction::ACTION_ABANDON => pht('Abandoned'), DifferentialAction::ACTION_CLOSE => pht('Closed'), DifferentialAction::ACTION_REQUEST => pht('Requested A Review Of'), DifferentialAction::ACTION_RESIGN => pht('Resigned From'), DifferentialAction::ACTION_ADDREVIEWERS => pht('Added Reviewers'), DifferentialAction::ACTION_CLAIM => pht('Commandeered'), DifferentialAction::ACTION_REOPEN => pht('Reopened'), ); $name = idx($map, $this->getNewValue()); if ($name !== null) { return $name; } break; } return parent::getActionName(); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS; $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CC; break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED; break; } break; case self::TYPE_UPDATE: $old = $this->getOldValue(); if ($old === null) { $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST; } else { $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED; } break; case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS; break; } break; case PhabricatorTransactions::TYPE_COMMENT: case self::TYPE_INLINE: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT; break; } if (!$tags) { $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER; } return $tags; } public function getTitle() { $author_phid = $this->getAuthorPHID(); $author_handle = $this->renderHandleLink($author_phid); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_INLINE: return pht( '%s added inline comments.', $author_handle); case self::TYPE_UPDATE: - if ($new) { + if ($this->getMetadataValue('isCommitUpdate')) { + return pht( + 'This revision was automatically updated to reflect the '. + 'committed changes.'); + } else if ($new) { // TODO: Migrate to PHIDs and use handles here? if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) { return pht( '%s updated this revision to %s.', $author_handle, $this->renderHandleLink($new)); } else { return pht( '%s updated this revision.', $author_handle); } } else { return pht( '%s updated this revision.', $author_handle); } case self::TYPE_ACTION: - return DifferentialAction::getBasicStoryText($new, $author_handle); + switch ($new) { + case DifferentialAction::ACTION_CLOSE: + if (!$this->getMetadataValue('isCommitClose')) { + return DifferentialAction::getBasicStoryText( + $new, + $author_handle); + } + $commit_name = $this->renderHandleLink( + $this->getMetadataValue('commitPHID')); + $committer_phid = $this->getMetadataValue('committerPHID'); + $author_phid = $this->getMetadataValue('authorPHID'); + if ($this->getHandleIfExists($committer_phid)) { + $committer_name = $this->renderHandleLink($committer_phid); + } else { + $committer_name = $this->getMetadataValue('committerName'); + } + if ($this->getHandleIfExists($author_phid)) { + $author_name = $this->renderHandleLink($author_phid); + } else { + $author_name = $this->getMetadataValue('authorName'); + } + + if ($committer_name && ($committer_name != $author_name)) { + return pht( + 'Closed by commit %s (authored by %s, committed by %s).', + $commit_name, + $author_name, + $committer_name); + } else { + return pht( + 'Closed by commit %s (authored by %s).', + $commit_name, + $author_name); + } + break; + default: + return DifferentialAction::getBasicStoryText($new, $author_handle); + } + break; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return pht( 'This revision is now accepted and ready to land.'); case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return pht( 'This revision now requires changes to proceed.'); case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return pht( 'This revision now requires review to proceed.'); } } return parent::getTitle(); } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $author_link = $this->renderHandleLink($author_phid); $object_link = $this->renderHandleLink($object_phid); switch ($this->getTransactionType()) { case self::TYPE_INLINE: return pht( '%s added inline comments to %s.', $author_link, $object_link); case self::TYPE_UPDATE: return pht( '%s updated the diff for %s.', $author_link, $object_link); case self::TYPE_ACTION: switch ($new) { case DifferentialAction::ACTION_ACCEPT: return pht( '%s accepted %s.', $author_link, $object_link); case DifferentialAction::ACTION_REJECT: return pht( '%s requested changes to %s.', $author_link, $object_link); case DifferentialAction::ACTION_RETHINK: return pht( '%s planned changes to %s.', $author_link, $object_link); case DifferentialAction::ACTION_ABANDON: return pht( '%s abandoned %s.', $author_link, $object_link); case DifferentialAction::ACTION_CLOSE: - return pht( - '%s closed %s.', - $author_link, - $object_link); + if (!$this->getMetadataValue('isCommitClose')) { + return pht( + '%s closed %s.', + $author_link, + $object_link); + } else { + $commit_name = $this->renderHandleLink( + $this->getMetadataValue('commitPHID')); + $committer_phid = $this->getMetadataValue('committerPHID'); + $author_phid = $this->getMetadataValue('authorPHID'); + if ($this->getHandleIfExists($committer_phid)) { + $committer_name = $this->renderHandleLink($committer_phid); + } else { + $committer_name = $this->getMetadataValue('committerName'); + } + if ($this->getHandleIfExists($author_phid)) { + $author_name = $this->renderHandleLink($author_phid); + } else { + $author_name = $this->getMetadataValue('authorName'); + } + + if ($committer_name && ($committer_name != $author_name)) { + return pht( + '%s closed %s by commit %s (authored by %s).', + $author_link, + $object_link, + $commit_name, + $author_name); + } else { + return pht( + '%s closed %s by commit %s.', + $author_link, + $object_link, + $commit_name); + } + } + break; + case DifferentialAction::ACTION_REQUEST: return pht( '%s requested review of %s.', $author_link, $object_link); case DifferentialAction::ACTION_RECLAIM: return pht( '%s reclaimed %s.', $author_link, $object_link); case DifferentialAction::ACTION_RESIGN: return pht( '%s resigned from %s.', $author_link, $object_link); case DifferentialAction::ACTION_CLAIM: return pht( '%s commandeered %s.', $author_link, $object_link); case DifferentialAction::ACTION_REOPEN: return pht( '%s reopened %s.', $author_link, $object_link); } break; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return pht( '%s is now accepted and ready to land.', $object_link); case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return pht( '%s now requires changes to proceed.', $object_link); case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return pht( '%s now requires review to proceed.', $object_link); } } return parent::getTitleForFeed($story); } public function getIcon() { switch ($this->getTransactionType()) { case self::TYPE_INLINE: return 'fa-comment'; case self::TYPE_UPDATE: return 'fa-refresh'; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return 'fa-check'; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return 'fa-times'; case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return 'fa-undo'; } break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return 'fa-check'; case DifferentialAction::ACTION_ACCEPT: return 'fa-check-circle-o'; case DifferentialAction::ACTION_REJECT: return 'fa-times-circle-o'; case DifferentialAction::ACTION_ABANDON: return 'fa-plane'; case DifferentialAction::ACTION_RETHINK: return 'fa-headphones'; case DifferentialAction::ACTION_REQUEST: return 'fa-refresh'; case DifferentialAction::ACTION_RECLAIM: case DifferentialAction::ACTION_REOPEN: return 'fa-bullhorn'; case DifferentialAction::ACTION_RESIGN: return 'fa-flag'; case DifferentialAction::ACTION_CLAIM: return 'fa-flag'; } case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER: return 'fa-user'; } } return parent::getIcon(); } public function shouldDisplayGroupWith(array $group) { // Never group status changes with other types of actions, they're indirect // and don't make sense when combined with direct actions. $type_status = self::TYPE_STATUS; if ($this->getTransactionType() == $type_status) { return false; } foreach ($group as $xaction) { if ($xaction->getTransactionType() == $type_status) { return false; } } return parent::shouldDisplayGroupWith($group); } public function getColor() { switch ($this->getTransactionType()) { case self::TYPE_UPDATE: return PhabricatorTransactions::COLOR_SKY; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return PhabricatorTransactions::COLOR_GREEN; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return PhabricatorTransactions::COLOR_RED; case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return PhabricatorTransactions::COLOR_ORANGE; } break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return PhabricatorTransactions::COLOR_BLUE; case DifferentialAction::ACTION_ACCEPT: return PhabricatorTransactions::COLOR_GREEN; case DifferentialAction::ACTION_REJECT: return PhabricatorTransactions::COLOR_RED; case DifferentialAction::ACTION_ABANDON: return PhabricatorTransactions::COLOR_BLACK; case DifferentialAction::ACTION_RETHINK: return PhabricatorTransactions::COLOR_RED; case DifferentialAction::ACTION_REQUEST: return PhabricatorTransactions::COLOR_SKY; case DifferentialAction::ACTION_RECLAIM: return PhabricatorTransactions::COLOR_SKY; case DifferentialAction::ACTION_REOPEN: return PhabricatorTransactions::COLOR_SKY; case DifferentialAction::ACTION_RESIGN: return PhabricatorTransactions::COLOR_ORANGE; case DifferentialAction::ACTION_CLAIM: return PhabricatorTransactions::COLOR_YELLOW; } } return parent::getColor(); } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER: return pht( 'The reviewers you are trying to add are already reviewing '. 'this revision.'); } break; case DifferentialTransaction::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return pht('This revision is already closed.'); case DifferentialAction::ACTION_ABANDON: return pht('This revision has already been abandoned.'); case DifferentialAction::ACTION_RECLAIM: return pht( 'You can not reclaim this revision because his revision is '. 'not abandoned.'); case DifferentialAction::ACTION_REOPEN: return pht( 'You can not reopen this revision because this revision is '. 'not closed.'); case DifferentialAction::ACTION_RETHINK: return pht('This revision already requires changes.'); case DifferentialAction::ACTION_REQUEST: return pht('Review is already requested for this revision.'); case DifferentialAction::ACTION_RESIGN: return pht( 'You can not resign from this revision because you are not '. 'a reviewer.'); case DifferentialAction::ACTION_CLAIM: return pht( 'You can not commandeer this revision because you already own '. 'it.'); case DifferentialAction::ACTION_ACCEPT: return pht( 'You have already accepted this revision.'); case DifferentialAction::ACTION_REJECT: return pht( 'You have already requested changes to this revision.'); } break; } return parent::getNoEffectDescription(); } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index 5d6b786f36..a432e84853 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -1,530 +1,508 @@ commit; $author = $ref->getAuthor(); $message = $ref->getMessage(); $committer = $ref->getCommitter(); $hashes = $ref->getHashes(); $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); if (!$data) { $data = new PhabricatorRepositoryCommitData(); } $data->setCommitID($commit->getID()); $data->setAuthorName((string)$author); $data->setCommitDetail('authorName', $ref->getAuthorName()); $data->setCommitDetail('authorEmail', $ref->getAuthorEmail()); $data->setCommitDetail( 'authorPHID', $this->resolveUserPHID($commit, $author)); $data->setCommitMessage($message); if (strlen($committer)) { $data->setCommitDetail('committer', $committer); $data->setCommitDetail('committerName', $ref->getCommitterName()); $data->setCommitDetail('committerEmail', $ref->getCommitterEmail()); $data->setCommitDetail( 'committerPHID', $this->resolveUserPHID($commit, $committer)); } $repository = $this->repository; $author_phid = $data->getCommitDetail('authorPHID'); $committer_phid = $data->getCommitDetail('committerPHID'); $user = new PhabricatorUser(); if ($author_phid) { $user = $user->loadOneWhere( 'phid = %s', $author_phid); } $differential_app = 'PhabricatorDifferentialApplication'; $revision_id = null; if (PhabricatorApplication::isClassInstalled($differential_app)) { $field_values = id(new DiffusionLowLevelCommitFieldsQuery()) ->setRepository($repository) ->withCommitRef($ref) ->execute(); $revision_id = idx($field_values, 'revisionID'); if (!empty($field_values['reviewedByPHIDs'])) { $data->setCommitDetail( 'reviewerPHID', reset($field_values['reviewedByPHIDs'])); } $data->setCommitDetail('differential.revisionID', $revision_id); } if ($author_phid != $commit->getAuthorPHID()) { $commit->setAuthorPHID($author_phid); } $commit->setSummary($data->getSummary()); $commit->save(); // Figure out if we're going to try to "autoclose" related objects (e.g., // close linked tasks and related revisions) and, if not, record why we // aren't. Autoclose can be disabled for various reasons at the repository // or commit levels. $autoclose_reason = $repository->shouldSkipAutocloseCommit($commit); $data->setCommitDetail('autocloseReason', $autoclose_reason); $should_autoclose = $repository->shouldAutocloseCommit($commit); // When updating related objects, we'll act under an omnipotent user to // ensure we can see them, but take actions as either the committer or // author (if we recognize their accounts) or the Diffusion application // (if we do not). $actor = PhabricatorUser::getOmnipotentUser(); $acting_as_phid = nonempty( $committer_phid, $author_phid, id(new PhabricatorDiffusionApplication())->getPHID()); $conn_w = id(new DifferentialRevision())->establishConnection('w'); // NOTE: The `differential_commit` table has a unique ID on `commitPHID`, // preventing more than one revision from being associated with a commit. // Generally this is good and desirable, but with the advent of hash // tracking we may end up in a situation where we match several different // revisions. We just kind of ignore this and pick one, we might want to // revisit this and do something differently. (If we match several revisions // someone probably did something very silly, though.) $revision = null; if ($revision_id) { $revision_query = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer($actor) ->needReviewerStatus(true) ->needActiveDiffs(true); $revision = $revision_query->executeOne(); if ($revision) { if (!$data->getCommitDetail('precommitRevisionStatus')) { $data->setCommitDetail( 'precommitRevisionStatus', $revision->getStatus()); } $commit_drev = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV; id(new PhabricatorEdgeEditor()) ->addEdge($commit->getPHID(), $commit_drev, $revision->getPHID()) ->save(); queryfx( $conn_w, 'INSERT IGNORE INTO %T (revisionID, commitPHID) VALUES (%d, %s)', DifferentialRevision::TABLE_COMMIT, $revision->getID(), $commit->getPHID()); $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; $should_close = ($revision->getStatus() != $status_closed) && $should_autoclose; if ($should_close) { - $commit_name = $repository->formatCommitName( - $commit->getCommitIdentifier()); - - $committer_name = $this->loadUserName( - $committer_phid, - $data->getCommitDetail('committer'), - $actor); - - $author_name = $this->loadUserName( - $author_phid, - $data->getAuthorName(), - $actor); - - if ($committer_name && ($committer_name != $author_name)) { - $revision_update_comment = pht( - 'Closed by commit %s (authored by %s, committed by %s).', - $commit_name, - $author_name, - $committer_name); - } else { - $revision_update_comment = pht( - 'Closed by commit %s (authored by %s).', - $commit_name, - $author_name); - } + $commit_close_xaction = id(new DifferentialTransaction()) + ->setTransactionType(DifferentialTransaction::TYPE_ACTION) + ->setNewValue(DifferentialAction::ACTION_CLOSE) + ->setMetadataValue('isCommitClose', true); + + $commit_close_xaction->setMetadataValue( + 'commitPHID', + $commit->getPHID()); + $commit_close_xaction->setMetadataValue( + 'committerPHID', + $committer_phid); + $commit_close_xaction->setMetadataValue( + 'committerName', + $data->getCommitDetail('committer')); + $commit_close_xaction->setMetadataValue( + 'authorPHID', + $author_phid); + $commit_close_xaction->setMetadataValue( + 'authorName', + $data->getAuthorName()); + $commit_close_xaction->setMetadataValue( + 'commitHashes', + $hashes); $diff = $this->generateFinalDiff($revision, $acting_as_phid); $vs_diff = $this->loadChangedByCommit($revision, $diff); $changed_uri = null; if ($vs_diff) { $data->setCommitDetail('vsDiff', $vs_diff->getID()); $changed_uri = PhabricatorEnv::getProductionURI( '/D'.$revision->getID(). '?vs='.$vs_diff->getID(). '&id='.$diff->getID(). '#toc'); } $xactions = array(); - - $xactions[] = id(new DifferentialTransaction()) - ->setTransactionType(DifferentialTransaction::TYPE_ACTION) - ->setNewValue(DifferentialAction::ACTION_CLOSE); - $xactions[] = id(new DifferentialTransaction()) ->setTransactionType(DifferentialTransaction::TYPE_UPDATE) ->setIgnoreOnNoEffect(true) - ->setNewValue($diff->getPHID()); - - $xactions[] = id(new DifferentialTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) - ->setIgnoreOnNoEffect(true) - ->attachComment( - id(new DifferentialTransactionComment()) - ->setContent($revision_update_comment)); + ->setNewValue($diff->getPHID()) + ->setMetadataValue('isCommitUpdate', true); + $xactions[] = $commit_close_xaction; $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_DAEMON, array()); $editor = id(new DifferentialTransactionEditor()) ->setActor($actor) ->setActingAsPHID($acting_as_phid) ->setContinueOnMissingFields(true) ->setContentSource($content_source) ->setChangedPriorToCommitURI($changed_uri) ->setIsCloseByCommit(true); try { $editor->applyTransactions($revision, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { // NOTE: We've marked transactions other than the CLOSE transaction // as ignored when they don't have an effect, so this means that we // lost a race to close the revision. That's perfectly fine, we can // just continue normally. } } } } if ($should_autoclose) { $this->closeTasks( $actor, $acting_as_phid, $repository, $commit, $message); } $data->save(); $commit->writeImportStatusFlag( PhabricatorRepositoryCommit::IMPORTED_MESSAGE); } - private function loadUserName($user_phid, $default, PhabricatorUser $actor) { - if (!$user_phid) { - return $default; - } - $handle = id(new PhabricatorHandleQuery()) - ->setViewer($actor) - ->withPHIDs(array($user_phid)) - ->executeOne(); - - return '@'.$handle->getName(); - } - private function generateFinalDiff( DifferentialRevision $revision, $actor_phid) { $viewer = PhabricatorUser::getOmnipotentUser(); $drequest = DiffusionRequest::newFromDictionary(array( 'user' => $viewer, 'repository' => $this->repository, )); $raw_diff = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, $drequest, 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), )); // TODO: Support adds, deletes and moves under SVN. if (strlen($raw_diff)) { $changes = id(new ArcanistDiffParser())->parseDiff($raw_diff); } else { // This is an empty diff, maybe made with `git commit --allow-empty`. // NOTE: These diffs have the same tree hash as their ancestors, so // they may attach to revisions in an unexpected way. Just let this // happen for now, although it might make sense to special case it // eventually. $changes = array(); } $diff = DifferentialDiff::newFromRawChanges($changes) ->setRepositoryPHID($this->repository->getPHID()) ->setAuthorPHID($actor_phid) ->setCreationMethod('commit') ->setSourceControlSystem($this->repository->getVersionControlSystem()) ->setLintStatus(DifferentialLintStatus::LINT_SKIP) ->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP) ->setDateCreated($this->commit->getEpoch()) ->setDescription( 'Commit r'. $this->repository->getCallsign(). $this->commit->getCommitIdentifier()); // TODO: This is not correct in SVN where one repository can have multiple // Arcanist projects. $arcanist_project = id(new PhabricatorRepositoryArcanistProject()) ->loadOneWhere('repositoryID = %d LIMIT 1', $this->repository->getID()); if ($arcanist_project) { $diff->setArcanistProjectPHID($arcanist_project->getPHID()); } $parents = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, $drequest, 'diffusion.commitparentsquery', array( 'commit' => $this->commit->getCommitIdentifier(), )); if ($parents) { $diff->setSourceControlBaseRevision(head($parents)); } // TODO: Attach binary files. return $diff->save(); } private function loadChangedByCommit( DifferentialRevision $revision, DifferentialDiff $diff) { $repository = $this->repository; $vs_diff = id(new DifferentialDiffQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRevisionIDs(array($revision->getID())) ->needChangesets(true) ->setLimit(1) ->executeOne(); if (!$vs_diff) { return null; } if ($vs_diff->getCreationMethod() == 'commit') { return null; } $vs_changesets = array(); foreach ($vs_diff->getChangesets() as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $vs_diff); $path = ltrim($path, '/'); $vs_changesets[$path] = $changeset; } $changesets = array(); foreach ($diff->getChangesets() as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $diff); $path = ltrim($path, '/'); $changesets[$path] = $changeset; } if (array_fill_keys(array_keys($changesets), true) != array_fill_keys(array_keys($vs_changesets), true)) { return $vs_diff; } $file_phids = array(); foreach ($vs_changesets as $changeset) { $metadata = $changeset->getMetadata(); $file_phid = idx($metadata, 'new:binary-phid'); if ($file_phid) { $file_phids[$file_phid] = $file_phid; } } $files = array(); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } foreach ($changesets as $path => $changeset) { $vs_changeset = $vs_changesets[$path]; $file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid'); if ($file_phid) { if (!isset($files[$file_phid])) { return $vs_diff; } $drequest = DiffusionRequest::newFromDictionary(array( 'user' => PhabricatorUser::getOmnipotentUser(), 'initFromConduit' => false, 'repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier(), 'path' => $path, )); $corpus = DiffusionFileContentQuery::newFromDiffusionRequest($drequest) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->loadFileContent() ->getCorpus(); if ($files[$file_phid]->loadFileData() != $corpus) { return $vs_diff; } } else { $context = implode("\n", $changeset->makeChangesWithContext()); $vs_context = implode("\n", $vs_changeset->makeChangesWithContext()); // We couldn't just compare $context and $vs_context because following // diffs will be considered different: // // -(empty line) // -echo 'test'; // (empty line) // // (empty line) // -echo "test"; // -(empty line) $hunk = id(new DifferentialHunkModern())->setChanges($context); $vs_hunk = id(new DifferentialHunkModern())->setChanges($vs_context); if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() || $hunk->makeNewFile() != $vs_hunk->makeNewFile()) { return $vs_diff; } } } return null; } private function resolveUserPHID( PhabricatorRepositoryCommit $commit, $user_name) { return id(new DiffusionResolveUserQuery()) ->withCommit($commit) ->withName($user_name) ->execute(); } private function closeTasks( PhabricatorUser $actor, $acting_as, PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, $message) { $maniphest = 'PhabricatorManiphestApplication'; if (!PhabricatorApplication::isClassInstalled($maniphest)) { return; } $prefixes = ManiphestTaskStatus::getStatusPrefixMap(); $suffixes = ManiphestTaskStatus::getStatusSuffixMap(); $matches = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($message); $task_statuses = array(); foreach ($matches as $match) { $prefix = phutil_utf8_strtolower($match['prefix']); $suffix = phutil_utf8_strtolower($match['suffix']); $status = idx($suffixes, $suffix); if (!$status) { $status = idx($prefixes, $prefix); } foreach ($match['monograms'] as $task_monogram) { $task_id = (int)trim($task_monogram, 'tT'); $task_statuses[$task_id] = $status; } } if (!$task_statuses) { return; } $tasks = id(new ManiphestTaskQuery()) ->setViewer($actor) ->withIDs(array_keys($task_statuses)) ->execute(); foreach ($tasks as $task_id => $task) { $xactions = array(); $edge_type = ManiphestTaskHasCommitEdgeType::EDGECONST; $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $edge_type) ->setNewValue( array( '+' => array( $commit->getPHID() => $commit->getPHID(), ), )); $status = $task_statuses[$task_id]; if ($status) { if ($task->getStatus() != $status) { $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_STATUS) ->setNewValue($status); $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $status_message = pht( 'Closed by commit %s.', $commit_name); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new ManiphestTransactionComment()) ->setContent($status_message)); } } $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_DAEMON, array()); $editor = id(new ManiphestTransactionEditor()) ->setActor($actor) ->setActingAsPHID($acting_as) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) + ->setUnmentionablePHIDMap( + array($commit->getPHID() => $commit->getPHID())) ->setContentSource($content_source); $editor->applyTransactions($task, $xactions); } } }