diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index 21bbdc32b8..7e4b3e5ae8 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -1,850 +1,938 @@ getTransactionType()) { case self::TYPE_PROJECT_COLUMN: case self::TYPE_EDGE: case self::TYPE_UNBLOCK: return false; } return parent::shouldGenerateOldValue(); } public function getRemarkupBlocks() { $blocks = parent::getRemarkupBlocks(); switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: $blocks[] = $this->getNewValue(); break; } return $blocks; } public function getRequiredHandlePHIDs() { $phids = parent::getRequiredHandlePHIDs(); $new = $this->getNewValue(); $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_OWNER: if ($new) { $phids[] = $new; } if ($old) { $phids[] = $old; } break; case self::TYPE_PROJECT_COLUMN: $phids[] = $new['projectPHID']; $phids[] = head($new['columnPHIDs']); break; case self::TYPE_MERGED_INTO: $phids[] = $new; break; case self::TYPE_MERGED_FROM: $phids = array_merge($phids, $new); break; case self::TYPE_EDGE: $phids = array_mergev( array( $phids, array_keys(nonempty($old, array())), array_keys(nonempty($new, array())), )); break; case self::TYPE_ATTACH: $old = nonempty($old, array()); $new = nonempty($new, array()); $phids = array_mergev( array( $phids, array_keys(idx($new, 'FILE', array())), array_keys(idx($old, 'FILE', array())), )); break; case self::TYPE_UNBLOCK: foreach (array_keys($new) as $phid) { $phids[] = $phid; } break; + case self::TYPE_STATUS: + $commit_phid = $this->getMetadataValue('commitPHID'); + if ($commit_phid) { + $phids[] = $commit_phid; + } + break; } return $phids; } public function shouldHide() { switch ($this->getTransactionType()) { + case PhabricatorTransactions::TYPE_EDGE: + $commit_phid = $this->getMetadataValue('commitPHID'); + $edge_type = $this->getMetadataValue('edge:type'); + + if ($edge_type == ManiphestTaskHasCommitEdgeType::EDGECONST) { + if ($commit_phid) { + return true; + } + } + break; case self::TYPE_DESCRIPTION: case self::TYPE_PRIORITY: case self::TYPE_STATUS: if ($this->getOldValue() === null) { return true; } else { return false; } break; case self::TYPE_SUBPRIORITY: return true; case self::TYPE_PROJECT_COLUMN: $old_cols = idx($this->getOldValue(), 'columnPHIDs'); $new_cols = idx($this->getNewValue(), 'columnPHIDs'); $old_cols = array_values($old_cols); $new_cols = array_values($new_cols); sort($old_cols); sort($new_cols); return ($old_cols === $new_cols); } return parent::shouldHide(); } public function getActionStrength() { switch ($this->getTransactionType()) { case self::TYPE_TITLE: return 1.4; case self::TYPE_STATUS: return 1.3; case self::TYPE_OWNER: return 1.2; case self::TYPE_PRIORITY: return 1.1; } return parent::getActionStrength(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_OWNER: if ($this->getAuthorPHID() == $new) { return 'green'; } else if (!$new) { return 'black'; } else if (!$old) { return 'green'; } else { return 'green'; } case self::TYPE_STATUS: $color = ManiphestTaskStatus::getStatusColor($new); if ($color !== null) { return $color; } if (ManiphestTaskStatus::isOpenStatus($new)) { return 'green'; } else { return 'indigo'; } case self::TYPE_PRIORITY: if ($old == ManiphestTaskPriority::getDefaultPriority()) { return 'green'; } else if ($old > $new) { return 'grey'; } else { return 'yellow'; } case self::TYPE_MERGED_FROM: return 'orange'; case self::TYPE_MERGED_INTO: return 'indigo'; } return parent::getColor(); } public function getActionName() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht('Created'); } return pht('Retitled'); case self::TYPE_STATUS: $action = ManiphestTaskStatus::getStatusActionName($new); if ($action) { return $action; } $old_closed = ManiphestTaskStatus::isClosedStatus($old); $new_closed = ManiphestTaskStatus::isClosedStatus($new); if ($new_closed && !$old_closed) { return pht('Closed'); } else if (!$new_closed && $old_closed) { return pht('Reopened'); } else { return pht('Changed Status'); } case self::TYPE_DESCRIPTION: return pht('Edited'); case self::TYPE_OWNER: if ($this->getAuthorPHID() == $new) { return pht('Claimed'); } else if (!$new) { return pht('Up For Grabs'); } else if (!$old) { return pht('Assigned'); } else { return pht('Reassigned'); } case self::TYPE_PROJECT_COLUMN: return pht('Changed Project Column'); case self::TYPE_PRIORITY: if ($old == ManiphestTaskPriority::getDefaultPriority()) { return pht('Triaged'); } else if ($old > $new) { return pht('Lowered Priority'); } else { return pht('Raised Priority'); } case self::TYPE_EDGE: case self::TYPE_ATTACH: return pht('Attached'); case self::TYPE_UNBLOCK: $old_status = head($old); $new_status = head($new); $old_closed = ManiphestTaskStatus::isClosedStatus($old_status); $new_closed = ManiphestTaskStatus::isClosedStatus($new_status); if ($old_closed && !$new_closed) { return pht('Block'); } else if (!$old_closed && $new_closed) { return pht('Unblock'); } else { return pht('Blocker'); } case self::TYPE_MERGED_INTO: case self::TYPE_MERGED_FROM: return pht('Merged'); } return parent::getActionName(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_OWNER: return 'fa-user'; case self::TYPE_TITLE: if ($old === null) { return 'fa-pencil'; } return 'fa-pencil'; case self::TYPE_STATUS: $action = ManiphestTaskStatus::getStatusIcon($new); if ($action !== null) { return $action; } if (ManiphestTaskStatus::isClosedStatus($new)) { return 'fa-check'; } else { return 'fa-pencil'; } case self::TYPE_DESCRIPTION: return 'fa-pencil'; case self::TYPE_PROJECT_COLUMN: return 'fa-columns'; case self::TYPE_MERGED_INTO: return 'fa-check'; case self::TYPE_MERGED_FROM: return 'fa-compress'; case self::TYPE_PRIORITY: if ($old == ManiphestTaskPriority::getDefaultPriority()) { return 'fa-arrow-right'; } else if ($old > $new) { return 'fa-arrow-down'; } else { return 'fa-arrow-up'; } case self::TYPE_EDGE: case self::TYPE_ATTACH: return 'fa-thumb-tack'; case self::TYPE_UNBLOCK: return 'fa-shield'; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht( '%s created this task.', $this->renderHandleLink($author_phid)); } return pht( '%s changed the title from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); case self::TYPE_DESCRIPTION: return pht( '%s edited the task description.', $this->renderHandleLink($author_phid)); case self::TYPE_STATUS: $old_closed = ManiphestTaskStatus::isClosedStatus($old); $new_closed = ManiphestTaskStatus::isClosedStatus($new); $old_name = ManiphestTaskStatus::getTaskStatusName($old); $new_name = ManiphestTaskStatus::getTaskStatusName($new); + $commit_phid = $this->getMetadataValue('commitPHID'); + if ($new_closed && !$old_closed) { if ($new == ManiphestTaskStatus::getDuplicateStatus()) { + if ($commit_phid) { + return pht( + '%s closed this task as a duplicate by committing %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($commit_phid)); + } else { + return pht( + '%s closed this task as a duplicate.', + $this->renderHandleLink($author_phid)); + } + } else { + if ($commit_phid) { + return pht( + '%s closed this task as "%s" by committing %s.', + $this->renderHandleLink($author_phid), + $new_name, + $this->renderHandleLink($commit_phid)); + } else { + return pht( + '%s closed this task as "%s".', + $this->renderHandleLink($author_phid), + $new_name); + } + } + } else if (!$new_closed && $old_closed) { + if ($commit_phid) { return pht( - '%s closed this task as a duplicate.', - $this->renderHandleLink($author_phid)); + '%s reopened this task as "%s" by committing %s.', + $this->renderHandleLink($author_phid), + $new_name, + $this->renderHandleLink($commit_phid)); } else { return pht( - '%s closed this task as "%s".', + '%s reopened this task as "%s".', $this->renderHandleLink($author_phid), $new_name); } - } else if (!$new_closed && $old_closed) { - return pht( - '%s reopened this task as "%s".', - $this->renderHandleLink($author_phid), - $new_name); } else { - return pht( - '%s changed the task status from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old_name, - $new_name); + if ($commit_phid) { + return pht( + '%s changed the task status from "%s" to "%s" by committing %s.', + $this->renderHandleLink($author_phid), + $old_name, + $new_name, + $this->renderHandleLink($commit_phid)); + } else { + return pht( + '%s changed the task status from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old_name, + $new_name); + } } case self::TYPE_UNBLOCK: $blocker_phid = key($new); $old_status = head($old); $new_status = head($new); $old_closed = ManiphestTaskStatus::isClosedStatus($old_status); $new_closed = ManiphestTaskStatus::isClosedStatus($new_status); $old_name = ManiphestTaskStatus::getTaskStatusName($old_status); $new_name = ManiphestTaskStatus::getTaskStatusName($new_status); if ($old_closed && !$new_closed) { return pht( '%s reopened blocking task %s as "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($blocker_phid), $new_name); } else if (!$old_closed && $new_closed) { return pht( '%s closed blocking task %s as "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($blocker_phid), $new_name); } else { return pht( '%s changed the status of blocking task %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($blocker_phid), $old_name, $new_name); } case self::TYPE_OWNER: if ($author_phid == $new) { return pht( '%s claimed this task.', $this->renderHandleLink($author_phid)); } else if (!$new) { return pht( '%s placed this task up for grabs.', $this->renderHandleLink($author_phid)); } else if (!$old) { return pht( '%s assigned this task to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); } else { return pht( '%s reassigned this task from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } case self::TYPE_PRIORITY: $old_name = ManiphestTaskPriority::getTaskPriorityName($old); $new_name = ManiphestTaskPriority::getTaskPriorityName($new); if ($old == ManiphestTaskPriority::getDefaultPriority()) { return pht( '%s triaged this task as "%s" priority.', $this->renderHandleLink($author_phid), $new_name); } else if ($old > $new) { return pht( '%s lowered the priority of this task from "%s" to "%s".', $this->renderHandleLink($author_phid), $old_name, $new_name); } else { return pht( '%s raised the priority of this task from "%s" to "%s".', $this->renderHandleLink($author_phid), $old_name, $new_name); } case self::TYPE_EDGE: // TODO: Remove this when we switch to real edges. Just reuse the // code in the parent; $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE); return $clone->getTitle(); case self::TYPE_ATTACH: $old = nonempty($old, array()); $new = nonempty($new, array()); $new = array_keys(idx($new, 'FILE', array())); $old = array_keys(idx($old, 'FILE', array())); $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s attached %d file(s): %s.', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s detached %d file(s): %s.', $this->renderHandleLink($author_phid), count($removed), $this->renderHandleList($removed)); } else { return pht( '%s changed file(s), attached %d: %s; detached %d: %s.', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } case self::TYPE_PROJECT_COLUMN: $project_phid = $new['projectPHID']; $column_phid = head($new['columnPHIDs']); return pht( '%s moved this task to %s on the %s workboard.', $this->renderHandleLink($author_phid), $this->renderHandleLink($column_phid), $this->renderHandleLink($project_phid)); break; case self::TYPE_MERGED_INTO: return pht( '%s closed this task as a duplicate of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); break; case self::TYPE_MERGED_FROM: return pht( '%s merged %d task(s): %s.', $this->renderHandleLink($author_phid), count($new), $this->renderHandleList($new)); break; } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } return pht( '%s renamed %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); case self::TYPE_DESCRIPTION: return pht( '%s edited the description of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_STATUS: $old_closed = ManiphestTaskStatus::isClosedStatus($old); $new_closed = ManiphestTaskStatus::isClosedStatus($new); $old_name = ManiphestTaskStatus::getTaskStatusName($old); $new_name = ManiphestTaskStatus::getTaskStatusName($new); + $commit_phid = $this->getMetadataValue('commitPHID'); + if ($new_closed && !$old_closed) { if ($new == ManiphestTaskStatus::getDuplicateStatus()) { + if ($commit_phid) { + return pht( + '%s closed %s as a duplicate by committing %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $this->renderHandleLink($commit_phid)); + } else { + return pht( + '%s closed %s as a duplicate.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + } else { + if ($commit_phid) { + return pht( + '%s closed %s as "%s" by committing %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $new_name, + $this->renderHandleLink($commit_phid)); + } else { + return pht( + '%s closed %s as "%s".', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $new_name); + } + } + } else if (!$new_closed && $old_closed) { + if ($commit_phid) { return pht( - '%s closed %s as a duplicate.', + '%s reopened %s as "%s" by committing %s.', $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid)); + $this->renderHandleLink($object_phid), + $new_name, + $this->renderHandleLink($commit_phid)); } else { return pht( - '%s closed %s as "%s".', + '%s reopened %s as "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $new_name); } - } else if (!$new_closed && $old_closed) { - return pht( - '%s reopened %s as "%s".', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid), - $new_name); } else { - return pht( - '%s changed the status of %s from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid), - $old_name, - $new_name); + if ($commit_phid) { + return pht( + '%s changed the status of %s from "%s" to "%s" by committing %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $old_name, + $new_name, + $this->renderHandleLink($commit_phid)); + } else { + return pht( + '%s changed the status of %s from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid), + $old_name, + $new_name); + } } case self::TYPE_UNBLOCK: $blocker_phid = key($new); $old_status = head($old); $new_status = head($new); $old_closed = ManiphestTaskStatus::isClosedStatus($old_status); $new_closed = ManiphestTaskStatus::isClosedStatus($new_status); $old_name = ManiphestTaskStatus::getTaskStatusName($old_status); $new_name = ManiphestTaskStatus::getTaskStatusName($new_status); if ($old_closed && !$new_closed) { return pht( '%s reopened %s, a task blocking %s, as "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($blocker_phid), $this->renderHandleLink($object_phid), $new_name); } else if (!$old_closed && $new_closed) { return pht( '%s closed %s, a task blocking %s, as "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($blocker_phid), $this->renderHandleLink($object_phid), $new_name); } else { return pht( '%s changed the status of %s, a task blocking %s, '. 'from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($blocker_phid), $this->renderHandleLink($object_phid), $old_name, $new_name); } case self::TYPE_OWNER: if ($author_phid == $new) { return pht( '%s claimed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else if (!$new) { return pht( '%s placed %s up for grabs.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else if (!$old) { return pht( '%s assigned %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($new)); } else { return pht( '%s reassigned %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } case self::TYPE_PRIORITY: $old_name = ManiphestTaskPriority::getTaskPriorityName($old); $new_name = ManiphestTaskPriority::getTaskPriorityName($new); if ($old == ManiphestTaskPriority::getDefaultPriority()) { return pht( '%s triaged %s as "%s" priority.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $new_name); } else if ($old > $new) { return pht( '%s lowered the priority of %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old_name, $new_name); } else { return pht( '%s raised the priority of %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old_name, $new_name); } case self::TYPE_EDGE: // TODO: Remove this when we switch to real edges. Just reuse the // code in the parent; $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE); return $clone->getTitleForFeed(); case self::TYPE_ATTACH: $old = nonempty($old, array()); $new = nonempty($new, array()); $new = array_keys(idx($new, 'FILE', array())); $old = array_keys(idx($old, 'FILE', array())); $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s attached %d file(s) of %s: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($added), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s detached %d file(s) of %s: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($removed), $this->renderHandleList($removed)); } else { return pht( '%s changed file(s) for %s, attached %d: %s; detached %d: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } case self::TYPE_PROJECT_COLUMN: $project_phid = $new['projectPHID']; $column_phid = head($new['columnPHIDs']); return pht( '%s moved %s to %s on the %s workboard.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($column_phid), $this->renderHandleLink($project_phid)); case self::TYPE_MERGED_INTO: return pht( '%s merged task %s into %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($new)); case self::TYPE_MERGED_FROM: return pht( '%s merged %d task(s) %s into %s.', $this->renderHandleLink($author_phid), count($new), $this->renderHandleList($new), $this->renderHandleLink($object_phid)); } return parent::getTitleForFeed(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_MERGED_INTO: case self::TYPE_STATUS: $tags[] = self::MAILTAG_STATUS; break; case self::TYPE_OWNER: $tags[] = self::MAILTAG_OWNER; break; case PhabricatorTransactions::TYPE_SUBSCRIBERS: $tags[] = self::MAILTAG_CC; break; case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST: $tags[] = self::MAILTAG_PROJECTS; break; default: $tags[] = self::MAILTAG_OTHER; break; } break; case self::TYPE_PRIORITY: $tags[] = self::MAILTAG_PRIORITY; break; case self::TYPE_UNBLOCK: $tags[] = self::MAILTAG_UNBLOCK; break; case self::TYPE_PROJECT_COLUMN: $tags[] = self::MAILTAG_COLUMN; break; case PhabricatorTransactions::TYPE_COMMENT: $tags[] = self::MAILTAG_COMMENT; break; default: $tags[] = self::MAILTAG_OTHER; break; } return $tags; } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case self::TYPE_STATUS: return pht('The task already has the selected status.'); case self::TYPE_OWNER: return pht('The task already has the selected owner.'); case self::TYPE_PRIORITY: return pht('The task already has the selected priority.'); } return parent::getNoEffectDescription(); } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index 57bed93f25..0e48901253 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -1,518 +1,510 @@ 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(id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(255) ->truncateString((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; $low_level_query = null; if (PhabricatorApplication::isClassInstalled($differential_app)) { $low_level_query = id(new DiffusionLowLevelCommitFieldsQuery()) ->setRepository($repository) ->withCommitRef($ref); $field_values = $low_level_query->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 = DiffusionCommitHasRevisionEdgeType::EDGECONST; 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_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()); if ($low_level_query) { $commit_close_xaction->setMetadataValue( 'revisionMatchData', $low_level_query->getRevisionMatchData()); $data->setCommitDetail( 'revisionMatchData', $low_level_query->getRevisionMatchData()); } $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_UPDATE) ->setIgnoreOnNoEffect(true) ->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 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($viewer, $changes) ->setRepositoryPHID($this->repository->getPHID()) ->setAuthorPHID($actor_phid) ->setCreationMethod('commit') ->setSourceControlSystem($this->repository->getVersionControlSystem()) ->setLintStatus(DifferentialLintStatus::LINT_AUTO_SKIP) ->setUnitStatus(DifferentialUnitStatus::UNIT_AUTO_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)) ->needProjectPHIDs(true) ->execute(); foreach ($tasks as $task_id => $task) { $xactions = array(); $edge_type = ManiphestTaskHasCommitEdgeType::EDGECONST; - $xactions[] = id(new ManiphestTransaction()) + $edge_xaction = 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) + ->setMetadataValue('commitPHID', $commit->getPHID()) ->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)); + $edge_xaction->setMetadataValue('commitPHID', $commit->getPHID()); } } + $xactions[] = $edge_xaction; + $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); } } }