diff --git a/src/applications/differential/constants/DifferentialLintStatus.php b/src/applications/differential/constants/DifferentialLintStatus.php index 2a519adc6c..5e16955f9b 100644 --- a/src/applications/differential/constants/DifferentialLintStatus.php +++ b/src/applications/differential/constants/DifferentialLintStatus.php @@ -1,12 +1,13 @@ getFieldName(); } public function getRequiredDiffPropertiesForRevisionView() { return array( 'arc:lint', 'arc:lint-excuse', 'arc:lint-postponed', ); } public function renderPropertyViewValue(array $handles) { $diff = $this->getObject()->getActiveDiff(); $path_changesets = mpull($diff->loadChangesets(), 'getID', 'getFilename'); $lstar = DifferentialRevisionUpdateHistoryView::renderDiffLintStar($diff); $lmsg = DifferentialRevisionUpdateHistoryView::getDiffLintMessage($diff); $ldata = $diff->getProperty('arc:lint'); $ltail = null; $rows = array(); $rows[] = array( 'style' => 'star', 'name' => $lstar, 'value' => $lmsg, 'show' => true, ); $excuse = $diff->getProperty('arc:lint-excuse'); if ($excuse) { $rows[] = array( 'style' => 'excuse', 'name' => 'Excuse', 'value' => phutil_escape_html_newlines($excuse), 'show' => true, ); } $show_limit = 10; $hidden = array(); if ($ldata) { $ldata = igroup($ldata, 'path'); foreach ($ldata as $path => $messages) { $rows[] = array( 'style' => 'section', 'name' => $path, 'show' => $show_limit, ); foreach ($messages as $message) { $path = idx($message, 'path'); $line = idx($message, 'line'); $code = idx($message, 'code'); $severity = idx($message, 'severity'); $name = idx($message, 'name'); $description = idx($message, 'description'); $line_link = 'line '.intval($line); if (isset($path_changesets[$path])) { $href = '#C'.$path_changesets[$path].'NL'.max(1, $line); // TODO: We are always showing the active diff // if ($diff->getID() != $this->getDiff()->getID()) { // $href = '/D'.$diff->getRevisionID().'?id='.$diff->getID().$href; // } $line_link = phutil_tag( 'a', array( 'href' => $href, ), $line_link); } if ($show_limit) { --$show_limit; $show = true; } else { $show = false; if (empty($hidden[$severity])) { $hidden[$severity] = 0; } $hidden[$severity]++; } $rows[] = array( 'style' => $this->getSeverityStyle($severity), 'name' => ucwords($severity), 'value' => hsprintf( '(%s) %s at %s', $code, $name, $line_link), 'show' => $show, ); if (!empty($message['locations'])) { $locations = array(); foreach ($message['locations'] as $location) { $other_line = idx($location, 'line'); $locations[] = idx($location, 'path', $path). ($other_line ? ":{$other_line}" : ''); } $description .= "\nOther locations: ".implode(', ', $locations); } if (strlen($description)) { $rows[] = array( 'style' => 'details', 'value' => phutil_escape_html_newlines($description), 'show' => false, ); if (empty($hidden['details'])) { $hidden['details'] = 0; } $hidden['details']++; } } } } $postponed = $diff->getProperty('arc:lint-postponed'); if ($postponed) { foreach ($postponed as $linter) { $rows[] = array( 'style' => $this->getPostponedStyle(), 'name' => 'Postponed', 'value' => $linter, 'show' => false, ); if (empty($hidden['postponed'])) { $hidden['postponed'] = 0; } $hidden['postponed']++; } } $show_string = $this->renderShowString($hidden); $view = new DifferentialResultsTableView(); $view->setRows($rows); $view->setShowMoreString($show_string); return $view->render(); } private function getSeverityStyle($severity) { $map = array( ArcanistLintSeverity::SEVERITY_ERROR => 'red', ArcanistLintSeverity::SEVERITY_WARNING => 'yellow', ArcanistLintSeverity::SEVERITY_AUTOFIX => 'yellow', ArcanistLintSeverity::SEVERITY_ADVICE => 'yellow', ); return idx($map, $severity); } private function getPostponedStyle() { return 'blue'; } private function renderShowString(array $hidden) { if (!$hidden) { return null; } // Reorder hidden things by severity. $hidden = array_select_keys( $hidden, array( ArcanistLintSeverity::SEVERITY_ERROR, ArcanistLintSeverity::SEVERITY_WARNING, ArcanistLintSeverity::SEVERITY_AUTOFIX, ArcanistLintSeverity::SEVERITY_ADVICE, 'details', 'postponed', )) + $hidden; $show = array(); foreach ($hidden as $key => $value) { switch ($key) { case ArcanistLintSeverity::SEVERITY_ERROR: $show[] = pht('%d Error(s)', $value); break; case ArcanistLintSeverity::SEVERITY_WARNING: $show[] = pht('%d Warning(s)', $value); break; case ArcanistLintSeverity::SEVERITY_AUTOFIX: $show[] = pht('%d Auto-Fix(es)', $value); break; case ArcanistLintSeverity::SEVERITY_ADVICE: $show[] = pht('%d Advice(s)', $value); break; case 'details': $show[] = pht('%d Detail(s)', $value); break; case 'postponed': $show[] = pht('%d Postponed', $value); break; default: $show[] = $value; break; } } return 'Show Full Lint Results ('.implode(', ', $show).')'; } public function getWarningsForDetailView() { $status = $this->getObject()->getActiveDiff()->getLintStatus(); if ($status < DifferentialLintStatus::LINT_WARN) { return array(); } + if ($status == DifferentialLintStatus::LINT_AUTO_SKIP) { + return array(); + } $warnings = array(); if ($status == DifferentialLintStatus::LINT_SKIP) { $warnings[] = pht( 'Lint was skipped when generating these changes.'); } else if ($status == DifferentialLintStatus::LINT_POSTPONED) { $warnings[] = pht( 'Background linting has not finished executing on these changes.'); } else { $warnings[] = pht('These changes have lint problems.'); } return $warnings; } } diff --git a/src/applications/differential/customfield/DifferentialUnitField.php b/src/applications/differential/customfield/DifferentialUnitField.php index de76056898..6f1f782ff3 100644 --- a/src/applications/differential/customfield/DifferentialUnitField.php +++ b/src/applications/differential/customfield/DifferentialUnitField.php @@ -1,226 +1,228 @@ getFieldName(); } public function getRequiredDiffPropertiesForRevisionView() { return array( 'arc:unit', 'arc:unit-excuse', ); } public function renderPropertyViewValue(array $handles) { $diff = $this->getObject()->getActiveDiff(); $ustar = DifferentialRevisionUpdateHistoryView::renderDiffUnitStar($diff); $umsg = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($diff); $rows = array(); $rows[] = array( 'style' => 'star', 'name' => $ustar, 'value' => $umsg, 'show' => true, ); $excuse = $diff->getProperty('arc:unit-excuse'); if ($excuse) { $rows[] = array( 'style' => 'excuse', 'name' => 'Excuse', 'value' => phutil_escape_html_newlines($excuse), 'show' => true, ); } $show_limit = 10; $hidden = array(); $udata = $diff->getProperty('arc:unit'); if ($udata) { $sort_map = array( ArcanistUnitTestResult::RESULT_BROKEN => 0, ArcanistUnitTestResult::RESULT_FAIL => 1, ArcanistUnitTestResult::RESULT_UNSOUND => 2, ArcanistUnitTestResult::RESULT_SKIP => 3, ArcanistUnitTestResult::RESULT_POSTPONED => 4, ArcanistUnitTestResult::RESULT_PASS => 5, ); foreach ($udata as $key => $test) { $udata[$key]['sort'] = idx($sort_map, idx($test, 'result')); } $udata = isort($udata, 'sort'); $engine = new PhabricatorMarkupEngine(); $engine->setViewer($this->getViewer()); $markup_objects = array(); foreach ($udata as $key => $test) { $userdata = idx($test, 'userdata'); if ($userdata) { if ($userdata !== false) { $userdata = str_replace("\000", '', $userdata); } $markup_object = id(new PhabricatorMarkupOneOff()) ->setContent($userdata) ->setPreserveLinebreaks(true); $engine->addObject($markup_object, 'default'); $markup_objects[$key] = $markup_object; } } $engine->process(); foreach ($udata as $key => $test) { $result = idx($test, 'result'); $default_hide = false; switch ($result) { case ArcanistUnitTestResult::RESULT_POSTPONED: case ArcanistUnitTestResult::RESULT_PASS: $default_hide = true; break; } if ($show_limit && !$default_hide) { --$show_limit; $show = true; } else { $show = false; if (empty($hidden[$result])) { $hidden[$result] = 0; } $hidden[$result]++; } $value = idx($test, 'name'); if (!empty($test['link'])) { $value = phutil_tag( 'a', array( 'href' => $test['link'], 'target' => '_blank', ), $value); } $rows[] = array( 'style' => $this->getResultStyle($result), 'name' => ucwords($result), 'value' => $value, 'show' => $show, ); if (isset($markup_objects[$key])) { $rows[] = array( 'style' => 'details', 'value' => $engine->getOutput($markup_objects[$key], 'default'), 'show' => false, ); if (empty($hidden['details'])) { $hidden['details'] = 0; } $hidden['details']++; } } } $show_string = $this->renderShowString($hidden); $view = new DifferentialResultsTableView(); $view->setRows($rows); $view->setShowMoreString($show_string); return $view->render(); } private function getResultStyle($result) { $map = array( ArcanistUnitTestResult::RESULT_PASS => 'green', ArcanistUnitTestResult::RESULT_FAIL => 'red', ArcanistUnitTestResult::RESULT_SKIP => 'blue', ArcanistUnitTestResult::RESULT_BROKEN => 'red', ArcanistUnitTestResult::RESULT_UNSOUND => 'yellow', ArcanistUnitTestResult::RESULT_POSTPONED => 'blue', ); return idx($map, $result); } private function renderShowString(array $hidden) { if (!$hidden) { return null; } // Reorder hidden things by severity. $hidden = array_select_keys( $hidden, array( ArcanistUnitTestResult::RESULT_BROKEN, ArcanistUnitTestResult::RESULT_FAIL, ArcanistUnitTestResult::RESULT_UNSOUND, ArcanistUnitTestResult::RESULT_SKIP, ArcanistUnitTestResult::RESULT_POSTPONED, ArcanistUnitTestResult::RESULT_PASS, 'details', )) + $hidden; $noun = array( ArcanistUnitTestResult::RESULT_BROKEN => 'Broken', ArcanistUnitTestResult::RESULT_FAIL => 'Failed', ArcanistUnitTestResult::RESULT_UNSOUND => 'Unsound', ArcanistUnitTestResult::RESULT_SKIP => 'Skipped', ArcanistUnitTestResult::RESULT_POSTPONED => 'Postponed', ArcanistUnitTestResult::RESULT_PASS => 'Passed', ); $show = array(); foreach ($hidden as $key => $value) { if ($key == 'details') { $show[] = pht('%d Detail(s)', $value); } else { $show[] = $value.' '.idx($noun, $key); } } return 'Show Full Unit Results ('.implode(', ', $show).')'; } public function getWarningsForDetailView() { $status = $this->getObject()->getActiveDiff()->getUnitStatus(); $warnings = array(); if ($status < DifferentialUnitStatus::UNIT_WARN) { // Don't show any warnings. + } else if ($status == DifferentialUnitStatus::UNIT_AUTO_SKIP) { + // Don't show any warnings. } else if ($status == DifferentialUnitStatus::UNIT_POSTPONED) { $warnings[] = pht( 'Background tests have not finished executing on these changes.'); } else if ($status == DifferentialUnitStatus::UNIT_SKIP) { $warnings[] = pht( 'Unit tests were skipped when generating these changes.'); } else { $warnings[] = pht('These changes have unit test problems.'); } return $warnings; } } diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php index 8d18656f12..463f6c1752 100644 --- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php +++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php @@ -1,422 +1,429 @@ diffs = $diffs; return $this; } public function setSelectedVersusDiffID($id) { $this->selectedVersusDiffID = $id; return $this; } public function setSelectedDiffID($id) { $this->selectedDiffID = $id; return $this; } public function setSelectedWhitespace($whitespace) { $this->selectedWhitespace = $whitespace; return $this; } public function setCommitsForLinks(array $commits) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $this->commitsForLinks = $commits; return $this; } public function render() { $this->requireResource('differential-core-view-css'); $this->requireResource('differential-revision-history-css'); $data = array( array( 'name' => 'Base', 'id' => null, 'desc' => 'Base', 'age' => null, 'obj' => null, ), ); $seq = 0; foreach ($this->diffs as $diff) { $data[] = array( 'name' => 'Diff '.(++$seq), 'id' => $diff->getID(), 'desc' => $diff->getDescription(), 'age' => $diff->getDateCreated(), 'obj' => $diff, ); } $max_id = $diff->getID(); $idx = 0; $rows = array(); $disable = false; $radios = array(); $last_base = null; $rowc = array(); foreach ($data as $row) { $diff = $row['obj']; $name = $row['name']; $id = $row['id']; $old_class = false; $new_class = false; if ($id) { $new_checked = ($this->selectedDiffID == $id); $new = javelin_tag( 'input', array( 'type' => 'radio', 'name' => 'id', 'value' => $id, 'checked' => $new_checked ? 'checked' : null, 'sigil' => 'differential-new-radio', )); if ($new_checked) { $new_class = true; $disable = true; } $new = phutil_tag( 'div', array( 'class' => 'differential-update-history-radio', ), $new); } else { $new = null; } if ($max_id != $id) { $uniq = celerity_generate_unique_node_id(); $old_checked = ($this->selectedVersusDiffID == $id); $old = phutil_tag( 'input', array( 'type' => 'radio', 'name' => 'vs', 'value' => $id, 'id' => $uniq, 'checked' => $old_checked ? 'checked' : null, 'disabled' => $disable ? 'disabled' : null, )); $radios[] = $uniq; if ($old_checked) { $old_class = true; } $old = phutil_tag( 'div', array( 'class' => 'differential-update-history-radio', ), $old); } else { $old = null; } $desc = $row['desc']; if ($row['age']) { $age = phabricator_datetime($row['age'], $this->getUser()); } else { $age = null; } if ($diff) { $lint = self::renderDiffLintStar($row['obj']); $lint = phutil_tag( 'div', array( 'class' => 'lintunit-star', 'title' => self::getDiffLintMessage($diff), ), $lint); $unit = self::renderDiffUnitStar($row['obj']); $unit = phutil_tag( 'div', array( 'class' => 'lintunit-star', 'title' => self::getDiffUnitMessage($diff), ), $unit); $base = $this->renderBaseRevision($diff); } else { $lint = null; $unit = null; $base = null; } if ($last_base !== null && $base !== $last_base) { // TODO: Render some kind of notice about rebases. } $last_base = $base; $id_link = phutil_tag( 'a', array( 'href' => '/differential/diff/'.$id.'/', ), $id); $rows[] = array( $name, $id_link, $base, $desc, $age, $lint, $unit, $old, $new, ); $classes = array(); if ($old_class) { $classes[] = 'differential-update-history-old-now'; } if ($new_class) { $classes[] = 'differential-update-history-new-now'; } $rowc[] = nonempty(implode(' ', $classes), null); } Javelin::initBehavior( 'differential-diff-radios', array( 'radios' => $radios, )); $options = array( DifferentialChangesetParser::WHITESPACE_IGNORE_FORCE => 'Ignore All', DifferentialChangesetParser::WHITESPACE_IGNORE_ALL => 'Ignore Most', DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING => 'Ignore Trailing', DifferentialChangesetParser::WHITESPACE_SHOW_ALL => 'Show All', ); foreach ($options as $value => $label) { $options[$value] = phutil_tag( 'option', array( 'value' => $value, 'selected' => ($value == $this->selectedWhitespace) ? 'selected' : null, ), $label); } $select = phutil_tag('select', array('name' => 'whitespace'), $options); $table = id(new AphrontTableView($rows)); $table->setHeaders( array( pht('Diff'), pht('ID'), pht('Base'), pht('Description'), pht('Created'), pht('Lint'), pht('Unit'), '', '', )); $table->setColumnClasses( array( 'pri', '', '', 'wide', 'date', 'center', 'center', 'center differential-update-history-old', 'center differential-update-history-new', )); $table->setRowClasses($rowc); $table->setDeviceVisibility( array( true, true, false, true, false, false, false, true, true, )); $show_diff = phutil_tag( 'div', array( 'class' => 'differential-update-history-footer', ), array( phutil_tag( 'label', array(), array( pht('Whitespace Changes:'), $select, )), phutil_tag( 'button', array(), pht('Show Diff')), )); $content = phabricator_form( $this->getUser(), array( 'action' => '#toc', ), array( $table, $show_diff, )); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Revision Update History')) ->setFlush(true) ->appendChild($content); } const STAR_NONE = 'none'; const STAR_OKAY = 'okay'; const STAR_WARN = 'warn'; const STAR_FAIL = 'fail'; const STAR_SKIP = 'skip'; public static function renderDiffLintStar(DifferentialDiff $diff) { static $map = array( DifferentialLintStatus::LINT_NONE => self::STAR_NONE, DifferentialLintStatus::LINT_OKAY => self::STAR_OKAY, DifferentialLintStatus::LINT_WARN => self::STAR_WARN, DifferentialLintStatus::LINT_FAIL => self::STAR_FAIL, DifferentialLintStatus::LINT_SKIP => self::STAR_SKIP, + DifferentialLintStatus::LINT_AUTO_SKIP => self::STAR_SKIP, DifferentialLintStatus::LINT_POSTPONED => self::STAR_SKIP ); $star = idx($map, $diff->getLintStatus(), self::STAR_FAIL); return self::renderDiffStar($star); } public static function renderDiffUnitStar(DifferentialDiff $diff) { static $map = array( DifferentialUnitStatus::UNIT_NONE => self::STAR_NONE, DifferentialUnitStatus::UNIT_OKAY => self::STAR_OKAY, DifferentialUnitStatus::UNIT_WARN => self::STAR_WARN, DifferentialUnitStatus::UNIT_FAIL => self::STAR_FAIL, DifferentialUnitStatus::UNIT_SKIP => self::STAR_SKIP, + DifferentialUnitStatus::UNIT_AUTO_SKIP => self::STAR_SKIP, DifferentialUnitStatus::UNIT_POSTPONED => self::STAR_SKIP, ); $star = idx($map, $diff->getUnitStatus(), self::STAR_FAIL); return self::renderDiffStar($star); } public static function getDiffLintMessage(DifferentialDiff $diff) { switch ($diff->getLintStatus()) { case DifferentialLintStatus::LINT_NONE: - return 'No Linters Available'; + return pht('No Linters Available'); case DifferentialLintStatus::LINT_OKAY: - return 'Lint OK'; + return pht('Lint OK'); case DifferentialLintStatus::LINT_WARN: - return 'Lint Warnings'; + return pht('Lint Warnings'); case DifferentialLintStatus::LINT_FAIL: - return 'Lint Errors'; + return pht('Lint Errors'); case DifferentialLintStatus::LINT_SKIP: - return 'Lint Skipped'; + return pht('Lint Skipped'); + case DifferentialLintStatus::LINT_AUTO_SKIP: + return pht('Automatic diff as part of commit; lint not applicable.'); case DifferentialLintStatus::LINT_POSTPONED: - return 'Lint Postponed'; + return pht('Lint Postponed'); } return '???'; } public static function getDiffUnitMessage(DifferentialDiff $diff) { switch ($diff->getUnitStatus()) { case DifferentialUnitStatus::UNIT_NONE: - return 'No Unit Test Coverage'; + return pht('No Unit Test Coverage'); case DifferentialUnitStatus::UNIT_OKAY: - return 'Unit Tests OK'; + return pht('Unit Tests OK'); case DifferentialUnitStatus::UNIT_WARN: - return 'Unit Test Warnings'; + return pht('Unit Test Warnings'); case DifferentialUnitStatus::UNIT_FAIL: - return 'Unit Test Errors'; + return pht('Unit Test Errors'); case DifferentialUnitStatus::UNIT_SKIP: - return 'Unit Tests Skipped'; + return pht('Unit Tests Skipped'); + case DifferentialUnitStatus::UNIT_AUTO_SKIP: + return pht( + 'Automatic diff as part of commit; unit tests not applicable.'); case DifferentialUnitStatus::UNIT_POSTPONED: - return 'Unit Tests Postponed'; + return pht('Unit Tests Postponed'); } return '???'; } private static function renderDiffStar($star) { $class = 'diff-star-'.$star; return phutil_tag( 'span', array('class' => $class), "\xE2\x98\x85"); } private function renderBaseRevision(DifferentialDiff $diff) { switch ($diff->getSourceControlSystem()) { case 'git': $base = $diff->getSourceControlBaseRevision(); if (strpos($base, '@') === false) { $label = substr($base, 0, 7); } else { // The diff is from git-svn $base = explode('@', $base); $base = last($base); $label = $base; } break; case 'svn': $base = $diff->getSourceControlBaseRevision(); $base = explode('@', $base); $base = last($base); $label = $base; break; default: $label = null; break; } $link = null; if ($label) { $commit_for_link = idx( $this->commitsForLinks, $diff->getSourceControlBaseRevision()); if ($commit_for_link) { $link = phutil_tag( 'a', array('href' => $commit_for_link->getURI()), $label); } else { $link = $label; } } return $link; } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index a432e84853..ef25a2ef34 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -1,508 +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_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_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($changes) ->setRepositoryPHID($this->repository->getPHID()) ->setAuthorPHID($actor_phid) ->setCreationMethod('commit') ->setSourceControlSystem($this->repository->getVersionControlSystem()) - ->setLintStatus(DifferentialLintStatus::LINT_SKIP) - ->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP) + ->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)) ->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); } } }