D1082: split: new extension to split changesets
quark (Jun Wu)
phabricator at mercurial-scm.org
Tue Dec 19 18:27:39 EST 2017
This revision was automatically updated to reflect the committed changes.
Closed by commit rHG02ea370c2baa: split: new extension to split changesets (authored by quark, committed by ).
REPOSITORY
rHG Mercurial
CHANGES SINCE LAST UPDATE
https://phab.mercurial-scm.org/D1082?vs=4555&id=4556
REVISION DETAIL
https://phab.mercurial-scm.org/D1082
AFFECTED FILES
hgext/split.py
tests/test-split.t
CHANGE DETAILS
diff --git a/tests/test-split.t b/tests/test-split.t
new file mode 100644
--- /dev/null
+++ b/tests/test-split.t
@@ -0,0 +1,525 @@
+#testcases obsstore-on obsstore-off
+
+ $ cat > $TESTTMP/editor.py <<EOF
+ > #!$PYTHON
+ > import os, sys
+ > path = os.path.join(os.environ['TESTTMP'], 'messages')
+ > messages = open(path).read().split('--\n')
+ > prompt = open(sys.argv[1]).read()
+ > sys.stdout.write(''.join('EDITOR: %s' % l for l in prompt.splitlines(True)))
+ > sys.stdout.flush()
+ > with open(sys.argv[1], 'w') as f:
+ > f.write(messages[0])
+ > with open(path, 'w') as f:
+ > f.write('--\n'.join(messages[1:]))
+ > EOF
+
+ $ cat >> $HGRCPATH <<EOF
+ > [extensions]
+ > drawdag=$TESTDIR/drawdag.py
+ > split=
+ > [ui]
+ > interactive=1
+ > [diff]
+ > git=1
+ > unified=0
+ > [alias]
+ > glog=log -G -T '{rev}:{node|short} {desc} {bookmarks}\n'
+ > EOF
+
+#if obsstore-on
+ $ cat >> $HGRCPATH <<EOF
+ > [experimental]
+ > evolution=all
+ > EOF
+#endif
+
+ $ hg init a
+ $ cd a
+
+Nothing to split
+
+ $ hg split
+ nothing to split
+ [1]
+
+ $ hg commit -m empty --config ui.allowemptycommit=1
+ $ hg split
+ abort: cannot split an empty revision
+ [255]
+
+ $ rm -rf .hg
+ $ hg init
+
+Cannot split working directory
+
+ $ hg split -r 'wdir()'
+ abort: cannot split working directory
+ [255]
+
+Generate some content
+
+ $ $TESTDIR/seq.py 1 5 >> a
+ $ hg ci -m a1 -A a -q
+ $ hg bookmark -i r1
+ $ sed 's/1/11/;s/3/33/;s/5/55/' a > b
+ $ mv b a
+ $ hg ci -m a2 -q
+ $ hg bookmark -i r2
+
+Cannot split a public changeset
+
+ $ hg phase --public -r 'all()'
+ $ hg split .
+ abort: cannot split public changeset
+ (see 'hg help phases' for details)
+ [255]
+
+ $ hg phase --draft -f -r 'all()'
+
+Cannot split while working directory is dirty
+
+ $ touch dirty
+ $ hg add dirty
+ $ hg split .
+ abort: uncommitted changes
+ [255]
+ $ hg forget dirty
+ $ rm dirty
+
+Split a head
+
+ $ cp -R . ../b
+ $ cp -R . ../c
+
+ $ hg bookmark r3
+
+ $ hg split 'all()'
+ abort: cannot split multiple revisions
+ [255]
+
+ $ runsplit() {
+ > cat > $TESTTMP/messages <<EOF
+ > split 1
+ > --
+ > split 2
+ > --
+ > split 3
+ > EOF
+ > cat <<EOF | hg split "$@"
+ > y
+ > y
+ > y
+ > y
+ > y
+ > y
+ > EOF
+ > }
+
+ $ HGEDITOR=false runsplit
+ diff --git a/a b/a
+ 1 hunks, 1 lines changed
+ examine changes to 'a'? [Ynesfdaq?] y
+
+ @@ -5,1 +5,1 @@ 4
+ -5
+ +55
+ record this change to 'a'? [Ynesfdaq?] y
+
+ transaction abort!
+ rollback completed
+ abort: edit failed: false exited with status 1
+ [255]
+ $ hg status
+
+ $ HGEDITOR="$PYTHON $TESTTMP/editor.py"
+ $ runsplit
+ diff --git a/a b/a
+ 1 hunks, 1 lines changed
+ examine changes to 'a'? [Ynesfdaq?] y
+
+ @@ -5,1 +5,1 @@ 4
+ -5
+ +55
+ record this change to 'a'? [Ynesfdaq?] y
+
+ EDITOR: HG: Splitting 1df0d5c5a3ab. Write commit message for the first split changeset.
+ EDITOR: a2
+ EDITOR:
+ EDITOR:
+ EDITOR: HG: Enter commit message. Lines beginning with 'HG:' are removed.
+ EDITOR: HG: Leave message empty to abort commit.
+ EDITOR: HG: --
+ EDITOR: HG: user: test
+ EDITOR: HG: branch 'default'
+ EDITOR: HG: changed a
+ created new head
+ diff --git a/a b/a
+ 1 hunks, 1 lines changed
+ examine changes to 'a'? [Ynesfdaq?] y
+
+ @@ -3,1 +3,1 @@ 2
+ -3
+ +33
+ record this change to 'a'? [Ynesfdaq?] y
+
+ EDITOR: HG: Splitting 1df0d5c5a3ab. So far it has been split into:
+ EDITOR: HG: - e704349bd21b: split 1
+ EDITOR: HG: Write commit message for the next split changeset.
+ EDITOR: a2
+ EDITOR:
+ EDITOR:
+ EDITOR: HG: Enter commit message. Lines beginning with 'HG:' are removed.
+ EDITOR: HG: Leave message empty to abort commit.
+ EDITOR: HG: --
+ EDITOR: HG: user: test
+ EDITOR: HG: branch 'default'
+ EDITOR: HG: changed a
+ diff --git a/a b/a
+ 1 hunks, 1 lines changed
+ examine changes to 'a'? [Ynesfdaq?] y
+
+ @@ -1,1 +1,1 @@
+ -1
+ +11
+ record this change to 'a'? [Ynesfdaq?] y
+
+ EDITOR: HG: Splitting 1df0d5c5a3ab. So far it has been split into:
+ EDITOR: HG: - e704349bd21b: split 1
+ EDITOR: HG: - a09ad58faae3: split 2
+ EDITOR: HG: Write commit message for the next split changeset.
+ EDITOR: a2
+ EDITOR:
+ EDITOR:
+ EDITOR: HG: Enter commit message. Lines beginning with 'HG:' are removed.
+ EDITOR: HG: Leave message empty to abort commit.
+ EDITOR: HG: --
+ EDITOR: HG: user: test
+ EDITOR: HG: branch 'default'
+ EDITOR: HG: changed a
+ saved backup bundle to $TESTTMP/a/.hg/strip-backup/1df0d5c5a3ab-8341b760-split.hg (glob) (obsstore-off !)
+
+#if obsstore-off
+ $ hg bookmark
+ r1 0:a61bcde8c529
+ r2 3:00eebaf8d2e2
+ * r3 3:00eebaf8d2e2
+ $ hg glog -p
+ @ 3:00eebaf8d2e2 split 3 r2 r3
+ | diff --git a/a b/a
+ | --- a/a
+ | +++ b/a
+ | @@ -1,1 +1,1 @@
+ | -1
+ | +11
+ |
+ o 2:a09ad58faae3 split 2
+ | diff --git a/a b/a
+ | --- a/a
+ | +++ b/a
+ | @@ -3,1 +3,1 @@
+ | -3
+ | +33
+ |
+ o 1:e704349bd21b split 1
+ | diff --git a/a b/a
+ | --- a/a
+ | +++ b/a
+ | @@ -5,1 +5,1 @@
+ | -5
+ | +55
+ |
+ o 0:a61bcde8c529 a1 r1
+ diff --git a/a b/a
+ new file mode 100644
+ --- /dev/null
+ +++ b/a
+ @@ -0,0 +1,5 @@
+ +1
+ +2
+ +3
+ +4
+ +5
+
+#else
+ $ hg bookmark
+ r1 0:a61bcde8c529
+ r2 4:00eebaf8d2e2
+ * r3 4:00eebaf8d2e2
+ $ hg glog
+ @ 4:00eebaf8d2e2 split 3 r2 r3
+ |
+ o 3:a09ad58faae3 split 2
+ |
+ o 2:e704349bd21b split 1
+ |
+ o 0:a61bcde8c529 a1 r1
+
+#endif
+
+Split a head while working parent is not that head
+
+ $ cd $TESTTMP/b
+
+ $ hg up 0 -q
+ $ hg bookmark r3
+
+ $ runsplit tip >/dev/null
+
+#if obsstore-off
+ $ hg bookmark
+ r1 0:a61bcde8c529
+ r2 3:00eebaf8d2e2
+ * r3 0:a61bcde8c529
+ $ hg glog
+ o 3:00eebaf8d2e2 split 3 r2
+ |
+ o 2:a09ad58faae3 split 2
+ |
+ o 1:e704349bd21b split 1
+ |
+ @ 0:a61bcde8c529 a1 r1 r3
+
+#else
+ $ hg bookmark
+ r1 0:a61bcde8c529
+ r2 4:00eebaf8d2e2
+ * r3 0:a61bcde8c529
+ $ hg glog
+ o 4:00eebaf8d2e2 split 3 r2
+ |
+ o 3:a09ad58faae3 split 2
+ |
+ o 2:e704349bd21b split 1
+ |
+ @ 0:a61bcde8c529 a1 r1 r3
+
+#endif
+
+Split a non-head
+
+ $ cd $TESTTMP/c
+ $ echo d > d
+ $ hg ci -m d1 -A d
+ $ hg bookmark -i d1
+ $ echo 2 >> d
+ $ hg ci -m d2
+ $ echo 3 >> d
+ $ hg ci -m d3
+ $ hg bookmark -i d3
+ $ hg up '.^' -q
+ $ hg bookmark d2
+ $ cp -R . ../d
+
+ $ runsplit -r 1 | grep rebasing
+ rebasing 2:b5c5ea414030 "d1" (d1)
+ rebasing 3:f4a0a8d004cc "d2" (d2)
+ rebasing 4:777940761eba "d3" (d3)
+#if obsstore-off
+ $ hg bookmark
+ d1 4:c4b449ef030e
+ * d2 5:c9dd00ab36a3
+ d3 6:19f476bc865c
+ r1 0:a61bcde8c529
+ r2 3:00eebaf8d2e2
+ $ hg glog -p
+ o 6:19f476bc865c d3 d3
+ | diff --git a/d b/d
+ | --- a/d
+ | +++ b/d
+ | @@ -2,0 +3,1 @@
+ | +3
+ |
+ @ 5:c9dd00ab36a3 d2 d2
+ | diff --git a/d b/d
+ | --- a/d
+ | +++ b/d
+ | @@ -1,0 +2,1 @@
+ | +2
+ |
+ o 4:c4b449ef030e d1 d1
+ | diff --git a/d b/d
+ | new file mode 100644
+ | --- /dev/null
+ | +++ b/d
+ | @@ -0,0 +1,1 @@
+ | +d
+ |
+ o 3:00eebaf8d2e2 split 3 r2
+ | diff --git a/a b/a
+ | --- a/a
+ | +++ b/a
+ | @@ -1,1 +1,1 @@
+ | -1
+ | +11
+ |
+ o 2:a09ad58faae3 split 2
+ | diff --git a/a b/a
+ | --- a/a
+ | +++ b/a
+ | @@ -3,1 +3,1 @@
+ | -3
+ | +33
+ |
+ o 1:e704349bd21b split 1
+ | diff --git a/a b/a
+ | --- a/a
+ | +++ b/a
+ | @@ -5,1 +5,1 @@
+ | -5
+ | +55
+ |
+ o 0:a61bcde8c529 a1 r1
+ diff --git a/a b/a
+ new file mode 100644
+ --- /dev/null
+ +++ b/a
+ @@ -0,0 +1,5 @@
+ +1
+ +2
+ +3
+ +4
+ +5
+
+#else
+ $ hg bookmark
+ d1 8:c4b449ef030e
+ * d2 9:c9dd00ab36a3
+ d3 10:19f476bc865c
+ r1 0:a61bcde8c529
+ r2 7:00eebaf8d2e2
+ $ hg glog
+ o 10:19f476bc865c d3 d3
+ |
+ @ 9:c9dd00ab36a3 d2 d2
+ |
+ o 8:c4b449ef030e d1 d1
+ |
+ o 7:00eebaf8d2e2 split 3 r2
+ |
+ o 6:a09ad58faae3 split 2
+ |
+ o 5:e704349bd21b split 1
+ |
+ o 0:a61bcde8c529 a1 r1
+
+#endif
+
+Split a non-head without rebase
+
+ $ cd $TESTTMP/d
+#if obsstore-off
+ $ runsplit -r 1 --no-rebase
+ abort: cannot split changeset with children without rebase
+ [255]
+#else
+ $ runsplit -r 1 --no-rebase >/dev/null
+ $ hg bookmark
+ d1 2:b5c5ea414030
+ * d2 3:f4a0a8d004cc
+ d3 4:777940761eba
+ r1 0:a61bcde8c529
+ r2 7:00eebaf8d2e2
+
+ $ hg glog
+ o 7:00eebaf8d2e2 split 3 r2
+ |
+ o 6:a09ad58faae3 split 2
+ |
+ o 5:e704349bd21b split 1
+ |
+ | o 4:777940761eba d3 d3
+ | |
+ | @ 3:f4a0a8d004cc d2 d2
+ | |
+ | o 2:b5c5ea414030 d1 d1
+ | |
+ | x 1:1df0d5c5a3ab a2
+ |/
+ o 0:a61bcde8c529 a1 r1
+
+#endif
+
+Split a non-head with obsoleted descendants
+
+#if obsstore-on
+ $ hg init $TESTTMP/e
+ $ cd $TESTTMP/e
+ $ hg debugdrawdag <<'EOS'
+ > H I J
+ > | | |
+ > F G1 G2 # amend: G1 -> G2
+ > | | / # prune: F
+ > C D E
+ > \|/
+ > B
+ > |
+ > A
+ > EOS
+ $ eval `hg tags -T '{tag}={node}\n'`
+ $ rm .hg/localtags
+ $ hg split $B --config experimental.evolution=createmarkers
+ abort: split would leave orphaned changesets behind
+ [255]
+ $ cat > $TESTTMP/messages <<EOF
+ > Split B
+ > EOF
+ $ cat <<EOF | hg split $B
+ > y
+ > y
+ > EOF
+ diff --git a/B b/B
+ new file mode 100644
+ examine changes to 'B'? [Ynesfdaq?] y
+
+ @@ -0,0 +1,1 @@
+ +B
+ \ No newline at end of file
+ record this change to 'B'? [Ynesfdaq?] y
+
+ EDITOR: HG: Splitting 112478962961. Write commit message for the first split changeset.
+ EDITOR: B
+ EDITOR:
+ EDITOR:
+ EDITOR: HG: Enter commit message. Lines beginning with 'HG:' are removed.
+ EDITOR: HG: Leave message empty to abort commit.
+ EDITOR: HG: --
+ EDITOR: HG: user: test
+ EDITOR: HG: branch 'default'
+ EDITOR: HG: added B
+ created new head
+ rebasing 2:26805aba1e60 "C"
+ rebasing 3:be0ef73c17ad "D"
+ rebasing 4:49cb92066bfd "E"
+ rebasing 7:97a6268cc7ef "G2"
+ rebasing 10:e2f1e425c0db "J"
+ $ hg glog -r 'sort(all(), topo)'
+ o 16:556c085f8b52 J
+ |
+ o 15:8761f6c9123f G2
+ |
+ o 14:a7aeffe59b65 E
+ |
+ | o 13:e1e914ede9ab D
+ |/
+ | o 12:01947e9b98aa C
+ |/
+ o 11:0947baa74d47 Split B
+ |
+ | o 9:88ede1d5ee13 I
+ | |
+ | x 6:af8cbf225b7b G1
+ | |
+ | x 3:be0ef73c17ad D
+ | |
+ | | o 8:74863e5b5074 H
+ | | |
+ | | x 5:ee481a2a1e69 F
+ | | |
+ | | x 2:26805aba1e60 C
+ | |/
+ | x 1:112478962961 B
+ |/
+ o 0:426bada5c675 A
+
+#endif
diff --git a/hgext/split.py b/hgext/split.py
new file mode 100644
--- /dev/null
+++ b/hgext/split.py
@@ -0,0 +1,177 @@
+# split.py - split a changeset into smaller ones
+#
+# Copyright 2015 Laurent Charignon <lcharignon at fb.com>
+# 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.
+"""command to split a changeset into smaller ones (EXPERIMENTAL)"""
+
+from __future__ import absolute_import
+
+from mercurial.i18n import _
+
+from mercurial.node import (
+ nullid,
+ short,
+)
+
+from mercurial import (
+ bookmarks,
+ cmdutil,
+ commands,
+ error,
+ hg,
+ obsolete,
+ phases,
+ registrar,
+ revsetlang,
+ scmutil,
+)
+
+# allow people to use split without explicitly enabling rebase extension
+from . import (
+ rebase,
+)
+
+cmdtable = {}
+command = registrar.command(cmdtable)
+
+# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
+# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
+# be specifying the version(s) of Mercurial they are tested with, or
+# leave the attribute unspecified.
+testedwith = 'ships-with-hg-core'
+
+ at command('^split',
+ [('r', 'rev', '', _("revision to split"), _('REV')),
+ ('', 'rebase', True, _('rebase descendants after split')),
+ ] + cmdutil.commitopts2,
+ _('hg split [--no-rebase] [[-r] REV]'))
+def split(ui, repo, *revs, **opts):
+ """split a changeset into smaller ones
+
+ Repeatedly prompt changes and commit message for new changesets until there
+ is nothing left in the original changeset.
+
+ If --rev was not given, split the working directory parent.
+
+ By default, rebase connected non-obsoleted descendants onto the new
+ changeset. Use --no-rebase to avoid the rebase.
+ """
+ revlist = []
+ if opts.get('rev'):
+ revlist.append(opts.get('rev'))
+ revlist.extend(revs)
+ with repo.wlock(), repo.lock(), repo.transaction('split') as tr:
+ revs = scmutil.revrange(repo, revlist or ['.'])
+ if len(revs) > 1:
+ raise error.Abort(_('cannot split multiple revisions'))
+
+ rev = revs.first()
+ ctx = repo[rev]
+ if rev is None or ctx.node() == nullid:
+ ui.status(_('nothing to split\n'))
+ return 1
+ if ctx.node() is None:
+ raise error.Abort(_('cannot split working directory'))
+
+ # rewriteutil.precheck is not very useful here because:
+ # 1. null check is done above and it's more friendly to return 1
+ # instead of abort
+ # 2. mergestate check is done below by cmdutil.bailifchanged
+ # 3. unstable check is more complex here because of --rebase
+ #
+ # So only "public" check is useful and it's checked directly here.
+ if ctx.phase() == phases.public:
+ raise error.Abort(_('cannot split public changeset'),
+ hint=_("see 'hg help phases' for details"))
+
+ descendants = list(repo.revs('(%d::) - (%d)', rev, rev))
+ alloworphaned = obsolete.isenabled(repo, obsolete.allowunstableopt)
+ if opts.get('rebase'):
+ # Skip obsoleted descendants and their descendants so the rebase
+ # won't cause conflicts for sure.
+ torebase = list(repo.revs('%ld - (%ld & obsolete())::',
+ descendants, descendants))
+ if not alloworphaned and len(torebase) != len(descendants):
+ raise error.Abort(_('split would leave orphaned changesets '
+ 'behind'))
+ else:
+ if not alloworphaned and descendants:
+ raise error.Abort(
+ _('cannot split changeset with children without rebase'))
+ torebase = ()
+
+ if len(ctx.parents()) > 1:
+ raise error.Abort(_('cannot split a merge changeset'))
+
+ cmdutil.bailifchanged(repo)
+
+ # Deactivate bookmark temporarily so it won't get moved unintentionally
+ bname = repo._activebookmark
+ if bname and repo._bookmarks[bname] != ctx.node():
+ bookmarks.deactivate(repo)
+
+ wnode = repo['.'].node()
+ top = None
+ try:
+ top = dosplit(ui, repo, tr, ctx, opts)
+ finally:
+ # top is None: split failed, need update --clean recovery.
+ # wnode == ctx.node(): wnode split, no need to update.
+ if top is None or wnode != ctx.node():
+ hg.clean(repo, wnode, show_stats=False)
+ if bname:
+ bookmarks.activate(repo, bname)
+ if torebase and top:
+ dorebase(ui, repo, torebase, top)
+
+def dosplit(ui, repo, tr, ctx, opts):
+ committed = [] # [ctx]
+
+ # Set working parent to ctx.p1(), and keep working copy as ctx's content
+ # NOTE: if we can have "update without touching working copy" API, the
+ # revert step could be cheaper.
+ hg.clean(repo, ctx.p1().node(), show_stats=False)
+ parents = repo.changelog.parents(ctx.node())
+ ui.pushbuffer()
+ cmdutil.revert(ui, repo, ctx, parents)
+ ui.popbuffer() # discard "reverting ..." messages
+
+ # Any modified, added, removed, deleted result means split is incomplete
+ incomplete = lambda repo: any(repo.status()[:4])
+
+ # Main split loop
+ while incomplete(repo):
+ if committed:
+ header = (_('HG: Splitting %s. So far it has been split into:\n')
+ % short(ctx.node()))
+ for c in committed:
+ firstline = c.description().split('\n', 1)[0]
+ header += _('HG: - %s: %s\n') % (short(c.node()), firstline)
+ header += _('HG: Write commit message for the next split '
+ 'changeset.\n')
+ else:
+ header = _('HG: Splitting %s. Write commit message for the '
+ 'first split changeset.\n') % short(ctx.node())
+ opts.update({
+ 'edit': True,
+ 'interactive': True,
+ 'message': header + ctx.description(),
+ })
+ commands.commit(ui, repo, **opts)
+ newctx = repo['.']
+ committed.append(newctx)
+
+ if not committed:
+ raise error.Abort(_('cannot split an empty revision'))
+
+ scmutil.cleanupnodes(repo, {ctx.node(): [c.node() for c in committed]},
+ operation='split')
+
+ return committed[-1]
+
+def dorebase(ui, repo, src, dest):
+ rebase.rebase(ui, repo, rev=[revsetlang.formatspec('%ld', src)],
+ dest=revsetlang.formatspec('%d', dest))
To: quark, #hg-reviewers
Cc: lothiraldan, martinvonz, dlax, mercurial-devel
More information about the Mercurial-devel
mailing list