fbamend: backport split command

Summary:
Backport the split command and its dependencies (namely, `bookmarksupdater`)
from mutable-history changeset cb0d62ed5e2a using GPL2 license.

There are some adjustments to make the ported code work well with existing
code, namely:

 - The `--norebase` flag was renamed as `--no-rebase` and moved to the
   backported command so we no longer wrap the split command.
 - Rebase now runs inside a same transaction.

The glob change in tests is because `(tip)` shows up.


Test Plan: arc unit

Reviewers: #mercurial, ikostia

Reviewed By: ikostia

Subscribers: ikostia, mjpieters, medson

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

Signature: t1:5254544:1497519470:fe8a7308b35b578fd4f1257cebba15062e752c2f
This commit is contained in:
Jun Wu 2017-06-16 13:59:12 -07:00
parent 08e9bc0bbf
commit 020ff84b79
4 changed files with 150 additions and 58 deletions

View File

@ -58,6 +58,7 @@ from . import (
movement,
restack,
revsets,
split,
unamend,
)
@ -66,8 +67,9 @@ revsetpredicate = revsets.revsetpredicate
cmdtable = {}
command = registrar.command(cmdtable)
cmdtable.update(unamend.cmdtable)
cmdtable.update(movement.cmdtable)
cmdtable.update(split.cmdtable)
cmdtable.update(unamend.cmdtable)
testedwith = 'ships-with-fb-hgext'
@ -124,25 +126,15 @@ def uisetup(ui):
evolvemod = extensions.find('evolve')
# Remove `hg previous`, `hg next` from evolve.
# Remove conflicted commands from evolve.
table = evolvemod.cmdtable
for name in ['prev', 'next']:
for name in ['prev', 'next', 'split']:
todelete = [k for k in table if name in k]
for k in todelete:
oldentry = table[k]
table['debugevolve%s' % name] = oldentry
del table[k]
# Wrap `hg split`.
splitentry = extensions.wrapcommand(
evolvemod.cmdtable,
'split',
wrapsplit,
)
splitentry[1].append(
('', 'norebase', False, _("don't rebase children after split"))
)
# Wrap `hg fold`.
foldentry = extensions.wrapcommand(
evolvemod.cmdtable,
@ -395,39 +387,6 @@ def fixupamend(ui, repo):
finally:
lockmod.release(wlock, lock, tr)
def wrapsplit(orig, ui, repo, *args, **opts):
"""Automatically rebase unstable descendants after split."""
# Find the rev number of the changeset to split. This needs to happen
# before splitting in case the input revset is relative to the working
# copy parent, since `hg split` may update to a new changeset.
revs = (list(args) + opts.get('rev', [])) or ['.']
rev = scmutil.revsingle(repo, revs[0]).rev()
torebase = repo.revs('descendants(%d) - (%d)', rev, rev)
# Perform split.
ret = orig(ui, repo, *args, **opts)
# Return early if split failed.
if ret:
return ret
# Fix up stack.
if not opts['norebase'] and torebase:
with repo.wlock():
with repo.lock():
with repo.transaction('splitrebase'):
top = repo.revs('allsuccessors(%d)', rev).last()
common.restackonce(ui, repo, top)
# The rebasestate file is incorrectly left behind, so cleanup.
# See the earlier comment on util.unlinkpath for more details.
util.unlinkpath(repo.vfs.join("rebasestate"),
ignoremissing=True)
# Fix up bookmarks, if any.
_fixbookmarks(repo, [rev])
return ret
def wrapfold(orig, ui, repo, *args, **opts):
"""Automatically rebase unstable descendants after fold."""
# Find the rev numbers of the changesets that will be folded. This needs

View File

@ -124,3 +124,18 @@ def latest(repo, rev):
"""
latest = repo.revs('allsuccessors(%d)', rev).last()
return latest if latest is not None else rev
def bookmarksupdater(repo, oldid, tr):
"""Return a callable update(newid) updating the current bookmark
and bookmarks bound to oldid to newid.
"""
def updatebookmarks(newid):
dirty = False
oldbookmarks = repo.nodebookmarks(oldid)
if oldbookmarks:
for b in oldbookmarks:
repo._bookmarks[b] = newid
dirty = True
if dirty:
repo._bookmarks.recordchange(tr)
return updatebookmarks

124
hgext3rd/fbamend/split.py Normal file
View File

@ -0,0 +1,124 @@
# split.py - split a changeset into smaller parts
#
# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
# Logilab SA <contact@logilab.fr>
# Pierre-Yves David <pierre-yves.david@ens-lyon.org>
# Patrick Mezard <patrick@mezard.eu>
# Copyright 2017 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import absolute_import
from mercurial import (
bookmarks,
cmdutil,
commands,
error,
hg,
lock as lockmod,
obsolete,
registrar,
scmutil,
)
from mercurial.i18n import _
from . import common
cmdtable = {}
command = registrar.command(cmdtable)
@command('^split',
[('r', 'rev', [], _("revision to split")),
('', 'no-rebase', False, _("don't rebase descendants after split")),
] + commands.commitopts + commands.commitopts2,
_('hg split [OPTION]... [-r] [REV]'))
def split(ui, repo, *revs, **opts):
"""split a changeset into smaller changesets
By default, split the current revision by prompting for all its hunks to be
redistributed into new changesets.
Use --rev to split a given changeset instead.
"""
tr = wlock = lock = None
newcommits = []
revarg = (list(revs) + opts.get('rev')) or ['.']
if len(revarg) != 1:
msg = _("more than one revset is given")
hnt = _("use either `hg split <rs>` or `hg split --rev <rs>`, not both")
raise error.Abort(msg, hint=hnt)
rev = scmutil.revsingle(repo, revarg[0])
if opts.get('no_rebase'):
torebase = ()
else:
torebase = repo.revs('descendants(%d) - (%d)', rev, rev)
try:
wlock = repo.wlock()
lock = repo.lock()
cmdutil.bailifchanged(repo)
tr = repo.transaction('split')
ctx = repo[rev]
r = ctx.rev()
disallowunstable = not obsolete.isenabled(repo,
obsolete.allowunstableopt)
if disallowunstable:
# XXX We should check head revs
if repo.revs("(%d::) - %d", rev, rev):
raise error.Abort(_("cannot split commit: %s not a head") % ctx)
if len(ctx.parents()) > 1:
raise error.Abort(_("cannot split merge commits"))
prev = ctx.p1()
bmupdate = common.bookmarksupdater(repo, ctx.node(), tr)
bookactive = repo._activebookmark
if bookactive is not None:
repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark)
bookmarks.deactivate(repo)
hg.update(repo, prev)
commands.revert(ui, repo, rev=r, all=True)
def haschanges():
modified, added, removed, deleted = repo.status()[:4]
return modified or added or removed or deleted
msg = ("HG: This is the original pre-split commit message. "
"Edit it as appropriate.\n\n")
msg += ctx.description()
opts['message'] = msg
opts['edit'] = True
while haschanges():
pats = ()
cmdutil.dorecord(ui, repo, commands.commit, 'commit', False,
cmdutil.recordfilter, *pats, **opts)
# TODO: Does no seem like the best way to do this
# We should make dorecord return the newly created commit
newcommits.append(repo['.'])
if haschanges():
if ui.prompt('Done splitting? [yN]', default='n') == 'y':
commands.commit(ui, repo, **opts)
newcommits.append(repo['.'])
break
else:
ui.status(_("no more change to split\n"))
if newcommits:
tip = repo[newcommits[-1]]
bmupdate(tip.node())
if bookactive is not None:
bookmarks.activate(repo, bookactive)
obsolete.createmarkers(repo, [(repo[r], newcommits)])
if torebase:
top = repo.revs('allsuccessors(%d)', rev).last()
common.restackonce(ui, repo, top, inhibithack=True)
tr.close()
finally:
tr.release()
lockmod.release(tr, lock, wlock)
# clean up possibly incorrect rebasestate
repo.vfs.tryunlink('rebasestate')

View File

@ -1,19 +1,13 @@
Set up test environment.
$ . $TESTDIR/require-ext.sh evolve
$ extpath=`dirname $TESTDIR`
$ cp $extpath/hgext3rd/allowunstable.py $TESTTMP
$ cat >> $HGRCPATH << EOF
> [extensions]
> allowunstable=$TESTTMP/allowunstable.py
> directaccess=$TESTDIR/../hgext3rd/directaccess.py
> evolve=
> fbamend=$TESTDIR/../hgext3rd/fbamend
> inhibit=$TESTDIR/../hgext3rd/inhibit.py
> rebase=
> strip=
> [experimental]
> evolution = createmarkers
> evolutioncommands = prev next fold split
> evolution = createmarkers, allowunstable
> [ui]
> interactive = true
> EOF
@ -117,7 +111,7 @@ Split in the middle of a stack.
created new head
Done splitting? [yN] y
rebasing 4:* "add d1 and d2" (glob)
rebasing 5:* "add d1 and d2" (glob)
rebasing 5:* "add d1 and d2"* (glob)
$ showgraph
o 9 add d1 and d2
@ -165,7 +159,7 @@ Split with multiple children and using hash.
rebasing 7:* "add c1 and c2" (glob)
rebasing 8:* "add d1 and d2" (glob)
rebasing 9:* "add d1 and d2" (glob)
rebasing 10:* "add d1 and d2" (glob)
rebasing 10:* "add d1 and d2"* (glob)
$ showgraph
o 18 add d1 and d2
@ -213,7 +207,7 @@ Split using revset.
rebasing 14:* "add c1 and c2" (glob)
rebasing 15:* "add c1 and c2" (glob)
rebasing 16:* "add d1 and d2" (glob)
rebasing 17:* "add d1 and d2" (glob)
rebasing 17:* "add d1 and d2"* (glob)
$ showgraph
o 23 add d1 and d2
@ -242,7 +236,7 @@ Test that command aborts when given multiple commits.
(use either `hg split <rs>` or `hg split --rev <rs>`, not both)
[255]
Test --norebase flag.
Test --no-rebase flag.
$ mkcommit e
created new head
$ hg rebase -s 20 -d .
@ -272,7 +266,7 @@ Test --norebase flag.
o 10 add d1 and d2
|
o 0 add a1 and a2
$ hg split --norebase << EOF
$ hg split --no-rebase << EOF
> y
> y
> n