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 @@ -21,18 +21,115 @@ hg, i18n, node, - registrar, ) _ = i18n._ cmdtable = {} -command = registrar.command(cmdtable) + +# Older veresions of Mercurial, ~4.7, moved the command function and the +# remoteopts object to different modules. +try: + from mercurial import registrar + command = registrar.command(cmdtable) +except: + command = cmdutil.command(cmdtable) + +try: + if "remoteopts" in cmdutil: + remoteopts = cmdutil.remoteopts +except: + from mercurial import commands + remoteopts = commands.remoteopts + +@command( + b'arc-amend', + [ + (b'l', + b'logfile', + b'', + _(b'read commit message from file'), + _(b'FILE')), + (b'm', + b'message', + b'', + _(b'use text as commit message'), + _(b'TEXT')), + (b'u', + b'user', + b'', + _(b'record the specified user as committer'), + _(b'USER')), + (b'd', + b'date', + b'', + _(b'record the specified date as commit date'), + _(b'DATE')), + (b'A', + b'addremove', + False, + _(b'mark new/missing files as added/removed before committing')), + (b'n', + b'note', + b'', + _(b'store a note on amend'), + _(b'TEXT')), + ], + _(b'[OPTION]')) +def amend(ui, repo, source=None, **opts): + """amend + + Uses Mercurial internal API to amend changes to a non-head commit. + + (This is an Arcanist extension to Mercurial.) + + Returns 0 if amending succeeds, 1 otherwise. + """ + + # The option keys seem to come in as 'str' type but the cmdutil.amend() code + # expects them as binary. To account for both Python 2 and Python 3 + # compatibility, insert the value under both 'str' and binary type, removing + # any whose values are not present (due to default values?). + newopts = {} + for key in opts: + val = opts.get(key) + newopts[key] = val + if isinstance(key, str): + newkey = key.encode('UTF-8') + newopts[newkey] = val + + orig = repo[b'.'] + extra = {} + pats = [] + cmdutil.amend(ui, repo, orig, extra, pats, newopts) + + """ + # This will allow running amend on older versions of Mercurial, ~3.5, however + # the behavior on those versions appears to squash child commits of the + # working directory into the amended commit. This behavior is undesired. + try: + cmdutil.amend(ui, repo, orig, extra, pats, newopts) + except: + def commitfunc(ui, repo, message, match, opts): + return repo.commit( + message, + opts.get('user') or orig.user(), + opts.get('date') or orig.date(), + match, + extra=extra) + cmdutil.amend(ui, repo, commitfunc, orig, extra, pats, newopts) + """ + + return 0 @command( b'arc-ls-markers', - [(b'', b'output', b'', - _(b'file to output refs to'), _(b'FILE')), - ] + cmdutil.remoteopts, + [ + (b'', + b'output', + b'', + _(b'file to output refs to'), + _(b'FILE')), + ] + remoteopts, _(b'[--output FILENAME] [SOURCE]')) def lsmarkers(ui, repo, source=None, **opts): """list markers