diff --git a/src/repository/api/base/ArcanistRepositoryAPI.php b/src/repository/api/base/ArcanistRepositoryAPI.php index 5db9e2d6..84149092 100644 --- a/src/repository/api/base/ArcanistRepositoryAPI.php +++ b/src/repository/api/base/ArcanistRepositoryAPI.php @@ -194,6 +194,14 @@ abstract class ArcanistRepositoryAPI { throw new ArcanistCapabilityNotSupportedException($this); } + public function getRelativeExplanation() { + throw new ArcanistCapabilityNotSupportedException($this); + } + + public function getCommitSummary($commit) { + throw new ArcanistCapabilityNotSupportedException($this); + } + public function getAllLocalChanges() { throw new ArcanistCapabilityNotSupportedException($this); } diff --git a/src/repository/api/git/ArcanistGitAPI.php b/src/repository/api/git/ArcanistGitAPI.php index d8f547ca..ff7db4df 100644 --- a/src/repository/api/git/ArcanistGitAPI.php +++ b/src/repository/api/git/ArcanistGitAPI.php @@ -25,6 +25,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { private $status; private $relativeCommit = null; + private $relativeExplanation = '???'; private $repositoryHasNoCommits = false; const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16; @@ -93,6 +94,10 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { $commits = array(); $info = trim($info, " \n\2"); + if (!strlen($info)) { + return array(); + } + $info = explode("\2", $info); foreach ($info as $line) { list($commit, $tree, $parents, $time, $author, $title, $message) @@ -128,6 +133,12 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { $this->relativeCommit = self::GIT_MAGIC_ROOT_COMMIT; + if ($this->repositoryHasNoCommits) { + $this->relativeExplanation = "the repository has no commits."; + } else { + $this->relativeExplanation = "the repository has only one commit."; + } + return $this->relativeCommit; } @@ -137,6 +148,10 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { if ($working_copy) { $default_relative = $working_copy->getConfig( 'git.default-relative-commit'); + $this->relativeExplanation = + "it is the merge-base of '{$default_relative}' and HEAD, as ". + "specified in 'git.default-relative-commit' in '.arcconfig'. This ". + "setting overrides other settings."; } if (!$default_relative) { @@ -145,12 +160,20 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { if (!$err) { $default_relative = trim($upstream); + $this->relativeExplanation = + "it is the merge-base of '{$default_relative}' (the Git upstream ". + "of the current branch) HEAD."; } } if (!$default_relative) { $default_relative = $this->readScratchFile('default-relative-commit'); $default_relative = trim($default_relative); + if ($default_relative) { + $this->relativeExplanation = + "it is the merge-base of '{$default_relative}' and HEAD, as ". + "specified in '.arc/default-relative-commit'."; + } } if (!$default_relative) { @@ -200,6 +223,9 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { // Don't perform this write until we've verified that the object is a // valid commit name. $this->writeScratchFile('default-relative-commit', $default_relative); + $this->relativeExplanation = + "it is the merge-base of '{$default_relative}' and HEAD, as you ". + "just specified."; } list($merge_base) = $this->execxLocal( @@ -658,6 +684,8 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { $base = reset($argv); if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) { $merge_base = $base; + $this->relativeExplanation = + "you explicitly specified the empty tree."; } else { list($err, $merge_base) = $this->execManualLocal( 'merge-base %s HEAD', @@ -666,6 +694,9 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { throw new ArcanistUsageException( "Unable to find any git commit named '{$base}' in this repository."); } + $this->relativeExplanation = + "it is the merge-base of '{$base}' and HEAD, as you explicitly ". + "specified."; } $this->setRelativeCommit(trim($merge_base)); } @@ -725,12 +756,14 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { $messages = $parser->parseDiff($messages); // First, try to find revisions by explicit revision IDs in commit messages. + $reason_map = array(); $revision_ids = array(); foreach ($messages as $message) { $object = ArcanistDifferentialCommitMessage::newFromRawCorpus( $message->getMetadata('message')); if ($object->getRevisionID()) { $revision_ids[] = $object->getRevisionID(); + $reason_map[$object->getRevisionID()] = $message->getCommitHash(); } } @@ -740,6 +773,13 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { $query + array( 'ids' => $revision_ids, )); + + foreach ($results as $key => $result) { + $hash = substr($reason_map[$result['id']], 0, 16); + $results[$key]['why'] = + "Commit message for '{$hash}' has explicit 'Differential Revision'."; + } + return $results; } @@ -756,6 +796,12 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { 'commitHashes' => $hashes, )); + foreach ($results as $key => $result) { + $results[$key]['why'] = + "A git commit or tree hash in the commit range is already attached ". + "to the Differential revision."; + } + return $results; } @@ -763,4 +809,21 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { $this->execxLocal('pull'); } + public function getRelativeExplanation() { + return $this->relativeExplanation; + } + + public function getCommitSummary($commit) { + if ($commit == self::GIT_MAGIC_ROOT_COMMIT) { + return '(The Empty Tree)'; + } + + list($summary) = $this->execxLocal( + 'log -n 1 --format=%C %s', + '%s', + $commit); + + return trim($summary); + } + } diff --git a/src/repository/api/mercurial/ArcanistMercurialAPI.php b/src/repository/api/mercurial/ArcanistMercurialAPI.php index 16284155..7ddbc0ba 100644 --- a/src/repository/api/mercurial/ArcanistMercurialAPI.php +++ b/src/repository/api/mercurial/ArcanistMercurialAPI.php @@ -26,6 +26,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { private $status; private $base; private $relativeCommit; + private $relativeExplanation; private $workingCopyRevision; private $localCommitInfo; private $includeDirectoryStateInDiffs; @@ -107,6 +108,10 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { } if (!$logs) { + + $this->relativeExplanation = + "you have no outgoing commits, so arc assumes you intend to submit ". + "uncommitted changes in the working copy."; // In Mercurial, we support operations against uncommitted changes. $this->setRelativeCommit($this->getWorkingCopyRevision()); return $this->relativeCommit; @@ -148,6 +153,15 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { } } + if ($against == 'null') { + $this->relativeExplanation = + "this is a new repository (all changes are outgoing)."; + } else { + $this->relativeExplanation = + "it is the first commit reachable from the working copy state ". + "which is not outgoing."; + } + $this->setRelativeCommit($against); } return $this->relativeCommit; @@ -163,8 +177,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { $this->getWorkingCopyRevision()); $logs = array_filter(explode("\2", $info)); - array_shift($logs); - $last_node = null; $is_first = true; @@ -200,6 +212,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { 'branch' => $branch, 'tag' => $tag, 'commit' => $node, + 'rev' => $node, // TODO: Remove eventually. 'local' => $rev, 'parents' => $commit_parents, 'summary' => head(explode("\n", $desc)), @@ -415,6 +428,9 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { if (count($argv) != 1) { throw new ArcanistUsageException("Specify only one commit."); } + + $this->relativeExplanation = "you explicitly specified it."; + // This does the "hg id" call we need to normalize/validate the revision // identifier. $this->setRelativeCommit(reset($argv)); @@ -481,7 +497,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { // Try to find revisions by hash. $hashes = array(); foreach ($this->getLocalCommitInformation() as $commit) { - $hashes[] = array('hgcm', $commit['rev']); + $hashes[] = array('hgcm', $commit['commit']); } $results = $conduit->callMethodSynchronous( @@ -490,6 +506,12 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { 'commitHashes' => $hashes, )); + foreach ($results as $key => $hash) { + $results[$key]['why'] = + "A mercurial commit hash in the commit range is already attached ". + "to the Differential revision."; + } + return $results; } @@ -519,4 +541,22 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { $this->localCommitInfo = null; } + public function getRelativeExplanation() { + return $this->relativeExplanation; + } + + public function getCommitSummary($commit) { + if ($commit == 'null') { + return '(The Empty Void)'; + } + + list($summary) = $this->execxLocal( + 'log --template {desc} --limit 1 --rev %s', + $commit); + + $summary = head(explode("\n", $summary)); + + return trim($summary); + } + } diff --git a/src/repository/api/subversion/ArcanistSubversionAPI.php b/src/repository/api/subversion/ArcanistSubversionAPI.php index ca987ba8..fa8e15b5 100644 --- a/src/repository/api/subversion/ArcanistSubversionAPI.php +++ b/src/repository/api/subversion/ArcanistSubversionAPI.php @@ -553,6 +553,11 @@ EODIFF; } } + foreach ($results as $key => $result) { + $results[$key]['why'] = + "Matching arcanist project name and working copy directory path."; + } + return $results; } diff --git a/src/workflow/which/ArcanistWhichWorkflow.php b/src/workflow/which/ArcanistWhichWorkflow.php index 56919bdf..beaec473 100644 --- a/src/workflow/which/ArcanistWhichWorkflow.php +++ b/src/workflow/which/ArcanistWhichWorkflow.php @@ -34,8 +34,8 @@ EOTEXT public function getCommandHelp() { return phutil_console_format(<< array( 'help' => "Show committed and abandoned revisions.", ), - 'id' => array( - 'help' => "If exactly one revision matches, print it to stdout. ". - "Otherwise, exit with an error. Intended for scripts.", - ), '*' => 'commit', ); } @@ -72,15 +68,63 @@ EOTEXT $repository_api = $this->getRepositoryAPI(); - $commit = $this->getArgument('commit'); - if (count($commit)) { + $arg_commit = $this->getArgument('commit'); + if (count($arg_commit)) { if (!$repository_api->supportsRelativeLocalCommits()) { throw new ArcanistUsageException( "This version control system does not support relative commits."); } else { - $repository_api->parseRelativeLocalCommit($commit); + $repository_api->parseRelativeLocalCommit($arg_commit); } } + $arg = $arg_commit ? ' '.head($arg_commit) : ''; + + if ($repository_api->supportsRelativeLocalCommits()) { + $relative = $repository_api->getRelativeCommit(); + + $info = $repository_api->getLocalCommitInformation(); + if ($info) { + $commits = array(); + foreach ($info as $commit) { + $hash = substr($commit['commit'], 0, 16); + $summary = $commit['summary']; + + $commits[] = " {$hash} {$summary}"; + } + $commits = implode("\n", $commits); + } else { + $commits = ' (No commits.)'; + } + + $explanation = $repository_api->getRelativeExplanation(); + + $relative_summary = $repository_api->getCommitSummary($relative); + $relative = substr($relative, 0, 16); + + if ($repository_api instanceof ArcanistGitAPI) { + $command = "git diff {$relative}..HEAD"; + } else if ($repository_api instanceof ArcanistMercurialAPI) { + $command = "hg diff --rev {$relative} --rev ."; + } else { + throw new Exception("Unknown VCS!"); + } + + echo phutil_console_wrap( + phutil_console_format( + "**RELATIVE COMMIT**\n". + "If you run 'arc diff{$arg}', changes between the commit:\n\n")); + + echo " {$relative} {$relative_summary}\n\n"; + echo phutil_console_wrap( + "...and the current working copy state will be sent to ". + "Differential, because {$explanation}\n\n". + "You can see the exact changes that will be sent by running ". + "this command:\n\n". + " $ {$command}\n\n". + "These commits will be included in the diff:\n\n"); + + echo $commits."\n\n\n"; + } $any_author = $this->getArgument('any-author'); $any_status = $this->getArgument('any-status'); @@ -98,23 +142,38 @@ EOTEXT $this->getConduit(), $query); + echo phutil_console_wrap( + phutil_console_format( + "**MATCHING REVISIONS**\n". + "These Differential revisions match the changes in this working ". + "copy:\n\n")); + if (empty($revisions)) { - $this->writeStatusMessage("No matching revisions.\n"); - return 1; - } - - if ($this->getArgument('id')) { - if (count($revisions) == 1) { - echo idx(head($revisions), 'id'); - return 0; - } else { - $this->writeStatusMessage("More than one matching revision.\n"); - return 1; + echo " (No revisions match.)\n"; + echo "\n"; + echo phutil_console_wrap( + phutil_console_format( + "Since there are no revisions in Differential which match this ". + "working copy, a new revision will be **created** if you run ". + "'arc diff{$arg}'.\n\n")); + } else { + foreach ($revisions as $revision) { + echo ' D'.$revision['id'].' '.$revision['title']."\n"; + echo ' Reason: '.$revision['why']."\n"; + echo "\n"; } - } - - foreach ($revisions as $revision) { - echo 'D'.$revision['id'].' '.$revision['title']."\n"; + if (count($revisions) == 1) { + echo phutil_console_wrap( + phutil_console_format( + "Since exactly one revision in Differential matches this working ". + "copy, it will be **updated** if you run 'arc diff{$arg}'.")); + } else { + echo phutil_console_wrap( + "Since more than one revision in Differential matches this working ". + "copy, you will be asked which revision you want to update if ". + "you run 'arc diff {$arg}'."); + } + echo "\n\n"; } return 0;