diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -803,8 +803,8 @@ // descendants and the min commit has no ancestors. The min/max terms are // used in a topological sense as chronological terms for commits can be // misleading or incorrect in certain situations. - $max_commit = last($commits)->getHash(); $min_commit = head($commits)->getHash(); + $max_commit = last($commits)->getHash(); $revision_ref = $set->getRevisionRef(); $commit_message = $revision_ref->getCommitMessage(); diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -658,37 +658,120 @@ public function doCommit($message) { $tmp_file = new TempFile(); Filesystem::writeFile($tmp_file, $message); - $this->execxLocal('commit -l %s', $tmp_file); + $this->execxLocal('commit --logfile %s', $tmp_file); $this->reloadWorkingCopy(); } public function amendCommit($message = null) { + $path_statuses = $this->buildUncommittedStatus(); + if ($message === null) { + if (empty($path_statuses)) { + // If there are no changes to the working directory and the message is + // not being changed then there's nothing to amend. + return; + } + $message = $this->getCommitMessage('.'); } $tmp_file = new TempFile(); Filesystem::writeFile($tmp_file, $message); - try { - $this->execxLocal( - 'commit --amend -l %s', - $tmp_file); - } catch (CommandException $ex) { - if (preg_match('/nothing changed/', $ex->getStdout())) { - // NOTE: Mercurial considers it an error to make a no-op amend. Although - // we generally defer to the underlying VCS to dictate behavior, this - // one seems a little goofy, and we use amend as part of various - // workflows under the assumption that no-op amends are fine. If this - // amend failed because it's a no-op, just continue. - } else { + if ($this->getMercurialFeature('evolve')) { + $this->execxLocal('amend --logfile %s --', $tmp_file); + try { + $this->execxLocal('evolve --all --'); + } catch (CommandException $ex) { + $this->execxLocal('evolve --abort --'); throw $ex; } + $this->reloadWorkingCopy(); + return; + } + + // Get the child nodes of the current changeset. + list($children) = $this->execxLocal( + 'log --template %s --rev %s --', + '{node} ', + 'children(.)'); + $child_nodes = array_filter(explode(' ', $children)); + + // For a head commit we can simply use `commit --amend` for both new commit + // message and amending changes from the working directory. + if (empty($child_nodes)) { + try { + $this->execxLocal('commit --amend --logfile %s --', $tmp_file); + } catch (CommandException $ex) { + if (preg_match('/nothing changed/', $ex->getStdout())) { + // NOTE: Mercurial considers it an error to make a no-op amend. + // Although we generally defer to the underlying VCS to dictate + // behavior, this one seems a little goofy, and we use amend as part + // of various workflows under the assumption that no-op amends are + // fine. If this amend failed because it's a no-op, just continue. + } else { + throw $ex; + } + } + } else { + $this->amendNonHeadCommit($child_nodes, $tmp_file); } $this->reloadWorkingCopy(); } + /** + * Amends a non-head commit with a new message and file changes. This + * strategy is for Mercurial repositories without the evolve extension. + * + * 1. Run 'arc-amend' which uses Mercurial internals to amend the current + * commit with updated message/file-changes. It results in a new commit + * from the right parent + * 2. For each branch from the original commit, rebase onto the new commit, + * removing the original branch. Note that there is potential for this to + * cause a conflict but this is something the user has to address. + * 3. Strip the original commit. + * + * @param array The list of child changesets off the original commit. + * @param file The file containing the new commit message. + */ + private function amendNonHeadCommit($child_nodes, $tmp_file) { + list($current) = $this->execxLocal( + 'log --template %s --rev . --', + '{node}'); + + $argv = array(); + foreach ($this->getMercurialExtensionArguments() as $arg) { + $argv[] = $arg; + } + $argv[] = 'arc-amend'; + $argv[] = '--logfile'; + $argv[] = $tmp_file; + $this->execxLocal('%Ls', $argv); + + list($new_commit) = $this->execxLocal( + 'log --rev tip --template %s --', + '{node}'); + + try { + foreach ($child_nodes as $child) { + // descendants(rev) will also include rev itself which is why this + // can't use a single rebase of descendants($current). + $revset = hgsprintf('descendants(%s)', $child); + $this->execxLocal( + 'rebase --dest %s --rev %s --', + $new_commit, + $revset); + } + } catch (CommandException $ex) { + $this->execxLocal('rebase --abort --'); + throw $ex; + } + + $this->execxLocal('--config extensions.strip= strip --rev %s --', + $current); + } + public function getCommitSummary($commit) { if ($commit == 'null') { return pht('(The Empty Void)'); diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php --- a/src/repository/state/ArcanistRepositoryLocalState.php +++ b/src/repository/state/ArcanistRepositoryLocalState.php @@ -192,10 +192,27 @@ return false; } + /** + * Stash uncommitted changes temporarily. Use {@method:restoreStash()} to + * bring these changes back. + * + * Note that on Git repositories the stash acts as a stack, so saving the + * stash must match appropriately to restoring the stash. + * + * @return wild On Git this returns true, on Mercurial this returns a name + * (string) which references the stash that was made. This name + * should later be passed to {@method:restoreStash()}. + */ protected function saveStash() { throw new PhutilMethodNotImplementedException(); } + /** + * Restores changes that were previously stashed by {@method:saveStash()}. + * + * @param wild On Git this parameter is unused, on Mercurial this should be + * the name (string) returned from {@method:saveStash()}. + */ protected function restoreStash($ref) { throw new PhutilMethodNotImplementedException(); } diff --git a/support/hg/arc-hg.py b/support/hg/arc-hg.py --- a/support/hg/arc-hg.py +++ b/support/hg/arc-hg.py @@ -28,6 +28,24 @@ cmdtable = {} command = registrar.command(cmdtable) +@command( + b'arc-amend', + [ + (b'l', b'logfile', b'', _(b'read commit message from file'), _(b'FILE')), + (b'm', b'message', None, _(b'use text as commit message'), _(b'TEXT')), + ], + _(b'[--logfile FILE] [--message TEXT]')) +def amend(ui, repo, source=None, **opts): + # The option keys seem to come in as 'str' type but the cmdutil.amend() code + # expects them as binary. + if opts.get('logfile'): + opts[b'logfile'] = opts.get('logfile') + if opts.get('message'): + opts[b'message'] = opts.get('message') + + cmdutil.amend(ui, repo, repo[b'.'], {}, [], opts) + return 0 + @command( b'arc-ls-markers', [(b'', b'output', b'',