mirror of
https://github.com/facebook/sapling.git
synced 2024-10-09 08:18:15 +03:00
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:
parent
1093ee0826
commit
bc3b0cd1c7
@ -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):
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user