Make hg next --rebase intelligently obsolete/inhibit changesets

Summary:
This change updates the behavior hg next --rebase. Specifically:

  - Only one changeset can be rebased at a time. If there are multiple candidate changesets, the command aborts.
  - Each time a changeset is rebased, its precursor is marked as obsolete, inhibition markers are stripped from it and its ancestors, and its preamend bookmark is deleted, if one exists.
  - The result of this is that if no non-obsolete changesets depend on the existence of the pre-rebased changeset, that changeset and its ancestors will be stripped, resulting in a cleaner user experience.
  - This change also adds back the --evolve flag, but makes it show in error instead of working. It turns out that removing the flag outright breaks the evolve extension.

Test Plan:
See updated unit tests for the exact commands to run to test this, as well as an overview of all of the new situations where behavior was changed.

A basic test plan would be:
1. Initialize a new repository, and create a stack of 4 commits.
2. Amend the second commit in the stack.
3. Do `hg next --rebase`. It should work as before.
4. Do `hg next --rebase` again. This time, the entire old stack should "disappear" from hg sl.

Additionally, attempting to run `hg next --rebase` when there are multiple possible child changesets should fail.

Reviewers: #sourcecontrol, durham

Reviewed By: durham

Subscribers: quark, mjpieters

Differential Revision: https://phabricator.intern.facebook.com/D3941922

Tasks: 13570554

Signature: t1:3941922:1475205056:58a8d1726cfcccbf14a38727be0220a09532ec97
This commit is contained in:
Arun Kulshreshtha 2016-09-30 10:40:58 -07:00
parent 1093ee0826
commit bc3b0cd1c7
2 changed files with 293 additions and 116 deletions

View File

@ -18,7 +18,15 @@ This extension is incompatible with changeset evolution. The command will
automatically disable itself if changeset evolution is enabled.
"""
from mercurial import util, cmdutil, phases, commands, bookmarks, repair
from mercurial import (
bookmarks,
cmdutil,
commands,
obsolete,
phases,
repair,
util,
)
from mercurial import merge, extensions, error, scmutil, hg, util
from mercurial.node import hex, nullid
from mercurial import obsolete
@ -32,6 +40,7 @@ command = cmdutil.command(cmdtable)
testedwith = 'internal'
rebasemod = None
inhibitmod = None
amendopts = [
('', 'rebase', None, _('rebases children after the amend')),
@ -77,19 +86,18 @@ def uisetup(ui):
def wrapnext(loaded):
if not loaded:
return
global inhibitmod
try:
inhibitmod = extensions.find('inhibit')
except KeyError:
pass
evolvemod = extensions.find('evolve')
entry = extensions.wrapcommand(evolvemod.cmdtable, 'next', nextrebase)
# Remove `hg next --evolve` and add `hg next --rebase`.
# Can't use a list comprehension since the list is in a tuple.
for i, opt in enumerate(entry[1]):
if opt[1] == 'evolve':
del entry[1][i]
break
entry[1].append((
'', 'rebase', False, _('rebase the changeset if necessary')
))
extensions.afterloaded('evolve', wrapnext)
def commit(orig, ui, repo, *pats, **opts):
@ -368,55 +376,121 @@ def fixupamend(ui, repo):
lockmod.release(wlock, lock, tr)
def nextrebase(orig, ui, repo, **opts):
# Disable `hg next --evolve`. The --rebase flag takes its place.
if opts['evolve']:
raise error.Abort(
_("the --evolve flag is not supported"),
hint=_("use 'hg next --rebase' instead")
)
# Just perform `hg next` if no --rebase option.
if not opts['rebase']:
return orig(ui, repo, **opts)
with nested(repo.wlock(), repo.lock()):
_nextrebase(orig, ui, repo, **opts)
def _nextrebase(orig, ui, repo, **opts):
"""Wrapper around the evolve extension's next command, adding the
--rebase option, which detects whether the current changeset has
any children on an obsolete precursor, and if so, rebases those
children onto the current version.
"""
# Just perform `hg next` if no --rebase option.
if not opts['rebase']:
return orig(ui, repo, **opts)
# Abort if there is an unfinished operation or changes to the
# working copy, to be consistent with the behavior of `hg next`.
cmdutil.checkunfinished(repo)
cmdutil.bailifchanged(repo)
# Find any child changesets on the changeset's precursor, if one exists.
# Find all children on the current changeset's obsolete precursors.
precursors = list(repo.set('allprecursors(.)'))
children = []
for p in repo.set('allprecursors(.)'):
children.extend(d.hex() for d in p.descendants())
for p in precursors:
children.extend(p.children())
# If there are no children on precursors, just do `hg next` normally.
if not children:
ui.warn(_("found no changesets to rebase, "
"doing normal 'hg next' instead\n"))
return orig(ui, repo, **opts)
current = repo['.']
child = children[0]
showopts = {'template': '[{shortest(node)}] {desc|firstline}\n'}
displayer = cmdutil.show_changeset(ui, repo, showopts)
# Catch the case where there are children on precursors, but
# there are also children on the current changeset.
if list(current.children()):
ui.warn(_("there are child changesets on one or more previous "
"versions of the current changeset, but the current "
"version also has children\n"))
ui.status(_("skipping rebasing the following child changesets:\n"))
for c in children:
displayer.show(c)
return orig(ui, repo, **opts)
# If there are several children on one or more precusors, it is
# ambiguous which changeset to rebase and update to.
if len(children) > 1:
ui.warn(_("there are multiple child changesets on previous versions "
"of the current changeset, namely:\n"))
for c in children:
displayer.show(c)
raise error.Abort(
_("ambiguous next changeset to rebase"),
hint=_("please rebase the desired one manually")
)
# If doing a dry run, just print out the corresponding commands.
if opts['dry_run']:
if children:
rev = '+'.join(children)
dest = repo['.'].hex()
ui.write(('hg rebase -r %s -d %s -k\n' % (rev, dest)))
ui.write(('hg rebase -r %s -d %s -k\n' % (child.hex(), current.hex())))
# Since we don't know what the new hashes will be until we actually
# perform the rebase, the dry run output can't explicitly say
# `hg update %s`. This is different from the normal output
# of `hg next --dry-run`.
ui.write(('hg next\n'))
return 0
return
# Rebase any children of the obsolete changesets.
if children:
rebaseopts = {
'rev': children,
'dest': repo['.'].hex(),
'keep': True,
}
# When the transaction closes, inhibition markers will be added back to
# changesets that have non-obsolete descendents, so those won't be
# "stripped". As such, we're relying on the inhibition markers to take
# care of the hard work of identifying which changesets not to strip.
with repo.transaction('nextrebase') as tr:
# Rebase any children of the obsolete changesets.
try:
rebasemod.rebase(ui, repo, **rebaseopts)
rebasemod.rebase(ui, repo, rev=[child.rev()], dest=current.rev(),
keep=True)
except error.InterventionRequired:
ui.status(_(
"please resolve any conflicts, run 'hg rebase --continue', "
"and then run 'hg next'\n"
))
tr.close()
raise
# Only call `hg next` if there were no conflicts.
# There isn't a good way of getting the newly rebased child changeset
# from rebasemod.rebase(), so just assume that it's the current
# changeset's only child. (This should always be the case.)
rebasedchild = current.children()[0]
ancestors = [r.node() for r in repo.set('%d %% .', child.rev())]
# Mark the old child changeset as obsolete, and remove the
# the inhibition markers from it and its ancestors. This
# effectively "strips" all of the obsoleted changesets in the
# stack below the child.
obsolete.createmarkers(repo, [(child, [rebasedchild])])
if inhibitmod:
inhibitmod._deinhibitmarkers(repo, ancestors)
# Remove any preamend bookmarks on precursors, as these would
# create unnecessary inhibition markers.
for p in precursors:
for bookmark in repo.nodebookmarks(p.node()):
if bookmark.endswith('.preamend'):
repo._bookmarks.pop(bookmark, None)
# Run `hg next` to update to the newly rebased child.
return orig(ui, repo, **opts)
def _preamendname(repo, node):

View File

@ -15,17 +15,41 @@ Set up test environment.
> evolutioncommands = prev next
> EOF
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
> echo "add $1" > msg
> hg ci -l msg
> echo "$1" > "$1"
> hg add "$1"
> echo "add $1" > msg
> hg ci -l msg
> }
$ reset() {
> cd ..
> rm -rf nextrebase
> hg init nextrebase
> cd nextrebase
> }
$ showgraph() {
> hg log --graph -T "{rev} {desc|firstline}"
> }
Create a situation where child commits are left behind after amend.
$ hg init nextrebase && cd nextrebase
Ensure that the hg next --evolve is disabled.
$ hg next --evolve
abort: the --evolve flag is not supported
(use 'hg next --rebase' instead)
[255]
Check case where there's nothing to rebase.
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[1] add b
$ hg next --rebase
found no changesets to rebase, doing normal 'hg next' instead
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
[2] add c
Create a situation where child commits are left behind after amend.
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[1] add b
@ -34,7 +58,7 @@ Create a situation where child commits are left behind after amend.
$ hg amend -m "add b and b2"
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg log --graph -T "{rev} {desc|firstline}"
$ showgraph
@ 4 add b and b2
|
| o 2 add c
@ -44,7 +68,7 @@ Create a situation where child commits are left behind after amend.
o 0 add a
Check to ensure hg rebase --next works.
Check that hg rebase --next works in the simple case.
$ hg next --rebase --dry-run
hg rebase -r 4538525df7e2b9f09423636c61ef63a4cb872a2d -d 29509da8015c02a5a44d703e561252f6478a1430 -k
hg next
@ -52,23 +76,22 @@ Check to ensure hg rebase --next works.
rebasing 2:4538525df7e2 "add c"
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
[5] add c
$ hg log --graph -T "{rev} {desc|firstline}"
$ showgraph
@ 5 add c
|
o 4 add b and b2
|
| o 2 add c
| |
| o 1 add b
|/
o 0 add a
Check whether it works with multiple children.
$ hg up 1
0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ hg strip 4+5
saved backup bundle to $TESTTMP/nextrebase/.hg/strip-backup/29509da8015c-5bd88862-backup.hg (glob)
Ensure we abort if there are multiple children on a precursor.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[1] add b
$ mkcommit d
created new head
$ hg prev
@ -79,10 +102,10 @@ Check whether it works with multiple children.
$ hg amend -m "add b and b3"
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg log --graph -T "{rev} {desc|firstline}"
@ 6 add b and b3
$ showgraph
@ 5 add b and b3
|
| o 4 add d
| o 3 add d
| |
| | o 2 add c
| |/
@ -92,66 +115,169 @@ Check whether it works with multiple children.
$ hg next --rebase
rebasing 2:4538525df7e2 "add c"
rebasing 4:78f83396d79e "add d"
ambigious next changeset:
[7] add c
[8] add d
explicitly update to one of them
[1]
$ hg log --graph -T "{rev} {desc|firstline}"
o 8 add d
there are multiple child changesets on previous versions of the current changeset, namely:
[4538] add c
[78f8] add d
abort: ambiguous next changeset to rebase
(please rebase the desired one manually)
[255]
Check behavior when there is a child on the current changeset and on
a precursor.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[1] add b
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ mkcommit d
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[4] add b
$ showgraph
o 5 add d
|
| o 7 add c
|/
@ 6 add b and b3
@ 4 add b
|
| o 4 add d
| o 2 add c
| |
| | o 2 add c
| |/
| o 1 add b
|/
o 0 add a
Check whether hg next --rebase behaves correctly when there is a conflict.
$ mkcommit conflict
created new head
$ hg next --rebase
there are child changesets on one or more previous versions of the current changeset, but the current version also has children
skipping rebasing the following child changesets:
[4538] add c
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
[5] add d
Check the case where multiple amends have occurred.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[6] add b and b3
[1] add b
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ echo b >> b
$ hg amend
$ echo b >> b
$ hg amend
$ showgraph
@ 8 add b
|
| o 2 add c
| |
| o 1 add b
|/
o 0 add a
$ hg next --rebase
rebasing 2:4538525df7e2 "add c"
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
[9] add c
$ showgraph
@ 9 add c
|
o 8 add b
|
o 0 add a
Check whether we can rebase a stack of commits.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit c
$ mkcommit d
$ hg up 1
0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ echo b >> b
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ showgraph
@ 5 add b
|
| o 3 add d
| |
| o 2 add c
| |
| o 1 add b
|/
o 0 add a
$ hg next --rebase
rebasing 2:4538525df7e2 "add c"
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
[6] add c
$ showgraph
@ 6 add c
|
o 5 add b
|
| o 3 add d
| |
| o 2 add c
| |
| o 1 add b
|/
o 0 add a
After rebasing the last commit in the stack, the old stack should be stripped.
$ hg next --rebase
rebasing 3:47d2a3944de8 "add d"
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
[7] add d
$ showgraph
@ 7 add d
|
o 6 add c
|
o 5 add b
|
o 0 add a
Check whether hg next --rebase behaves correctly when there is a conflict.
$ reset
$ mkcommit a
$ mkcommit b
$ mkcommit conflict
$ hg prev
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
[1] add b
$ echo "different" > conflict
$ hg add conflict
$ hg amend
warning: the changeset's children were left behind
(use 'hg amend --fixup' to rebase them)
$ hg log --graph -T "{rev} {desc|firstline}"
@ 11 add b and b3
$ showgraph
@ 4 add b
|
| o 9 add conflict
| o 2 add conflict
| |
| | o 8 add d
| |/
| | o 7 add c
| |/
| o 6 add b and b3
|/
| o 4 add d
| |
| | o 2 add c
| |/
| o 1 add b
|/
o 0 add a
$ hg next --rebase
rebasing 2:4538525df7e2 "add c"
rebasing 4:78f83396d79e "add d"
rebasing 7:2e88ee75f11f "add c"
rebasing 8:1a847fbbfbb6 "add d"
rebasing 9:b8431585b2c3 "add conflict"
rebasing 2:391efaa4d81f "add conflict"
merging conflict
warning: conflicts while merging conflict! (edit, then use 'hg resolve --mark')
please resolve any conflicts, run 'hg rebase --continue', and then run 'hg next'
@ -161,42 +287,19 @@ Check whether hg next --rebase behaves correctly when there is a conflict.
abort: rebase in progress
(use 'hg rebase --continue' or 'hg rebase --abort')
[255]
$ rm conflict
$ echo "merged" > conflict
$ hg resolve --mark conflict
(no more unresolved files)
continue: hg rebase --continue
$ hg rebase --continue
already rebased 2:4538525df7e2 "add c" as 5e344ef92cb2
already rebased 4:78f83396d79e "add d" as 97c68b3e05f1
already rebased 7:2e88ee75f11f "add c" as 198890c29490
already rebased 8:1a847fbbfbb6 "add d" as ccdc055b4afe
rebasing 9:b8431585b2c3 "add conflict"
$ hg log --graph -T "{rev} {desc|firstline}"
o 16 add conflict
rebasing 2:391efaa4d81f "add conflict"
$ showgraph
o 5 add conflict
|
| o 15 add d
|/
| o 14 add c
|/
| o 13 add d
|/
| o 12 add c
|/
@ 11 add b and b3
@ 4 add b
|
| o 9 add conflict
| o 2 add conflict
| |
| | o 8 add d
| |/
| | o 7 add c
| |/
| o 6 add b and b3
|/
| o 4 add d
| |
| | o 2 add c
| |/
| o 1 add b
|/
o 0 add a