Patchwork D1082: split: new extension to split changesets

login
register
mail settings
Submitter phabricator
Date Oct. 14, 2017, 9:15 p.m.
Message ID <differential-rev-PHID-DREV-q6q64spemn32iqlebv7u-req@phab.mercurial-scm.org>
Download mbox | patch
Permalink /patch/24923/
State Superseded
Headers show

Comments

phabricator - Oct. 14, 2017, 9:15 p.m.
quark created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  This diff introduces an experimental split extension to split changesets.
  
  The implementation is largely inspired by Laurent Charignon's implementation
  for mutable-history (changeset 9603aa1ecdfd54b0d86e262318a72e0a2ffeb6cc [1])
  
  This version contains various improvements:
  
  - Rebase by default This is more friendly for new users. Split won't lead to merge conflicts so a rebase won't give the user more trouble. This has been on by default at Facebook for months now and seems to be a good UX improvement. The rebase skips obsoleted or orphaned changesets, which can avoid issues like allowdivergence, merge conflicts, etc. This is more flexible because the user can decide what to do next (see the last test case in test-split.t)
  - Remove "Done split? [y/n]" prompt That could be detected by checking `repo.status()` instead.
  - Works with obsstore disabled Without obsstore, split uses strip to clean up old nodes, and it can even handle split a non-head changeset with "allowunstable" disabled, since it runs a rebase to solve the "unstable" issue in a same transaction.
  - More friendly editor text Put what has been already split into the editor text so users won't lost track about where they are.
  
  [1]: https://bitbucket.org/marmoute/mutable-history/commits/9603aa1ecdfd54b

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

AFFECTED FILES
  hgext/split.py
  tests/test-split.t

CHANGE DETAILS




To: quark, #hg-reviewers
Cc: mercurial-devel
phabricator - Oct. 15, 2017, 9:03 a.m.
dlax added inline comments.

INLINE COMMENTS

> split.py:53
> +
> +    Repetitively prompt changes and commit message for new changesets until
> +    there is nothing left in the original changeset.

Not sure "repetitively" exists. Maybe "repeatedly"?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: dlax, mercurial-devel
phabricator - Oct. 16, 2017, 5:36 p.m.
quark added a comment.


  I changed it to "repeatedly". The word "repetively" seems to exist but may have negative tone according to https://www.usingenglish.com/forum/threads/12782-Need-advice-on-the-difference-btw-Repeatedly-and-Repetitively-thanks-teachers!

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: dlax, mercurial-devel
phabricator - Oct. 16, 2017, 5:44 p.m.
martinvonz added inline comments.

INLINE COMMENTS

> test-split.t:487-493
> +  | o  9:88ede1d5ee13 I
> +  | |
> +  | x  6:af8cbf225b7b G1
> +  | |
> +  | x  3:be0ef73c17ad D
> +  | |
> +  | | o  8:74863e5b5074 H

Leaving these two behind seems reasonable. It would also be reasonable to evolve/stabilize them. Either way, it's different from what "hg rebase" does. Do we eventually want them to behave the same? If so, are we okay with a small BC break here (either in split or in rebase)?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: martinvonz, dlax, mercurial-devel
phabricator - Oct. 16, 2017, 5:49 p.m.
lothiraldan added a comment.


  It will be great to have split in core, even if it's only as an experimental experiment for now.
  
  I like the UX improvements, but could we add a config knob to disable the auto-rebase for power-users? I agree that generating orphans is maybe not the best UX for users, so I think having it on by default could be an improvement.
  
  But, I often am in the middle of a too big stack and auto-rebasing would break my flow of fixing commits from bottom to top without mentioning the number of obs-markers it would generate.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: lothiraldan, martinvonz, dlax, mercurial-devel
phabricator - Oct. 16, 2017, 6:54 p.m.
quark added a comment.


  In https://phab.mercurial-scm.org/D1082#18648, @lothiraldan wrote:
  
  > It will be great to have split in core, even if it's only as an experimental experiment for now.
  >
  > I like the UX improvements, but could we add a config knob to disable the auto-rebase for power-users? I agree that generating orphans is maybe not the best UX for users, so I think having it on by default could be an improvement.
  
  
  I believe most users want auto-rebase by default. Auto-rebase is the default at FB for months and people like it.
  
  I agree power users may want a different default. In that case, you can set alias `split = split --no-rebase`.
  
  > But, I often am in the middle of a too big stack and auto-rebasing would break my flow of fixing commits from bottom to top without mentioning the number of obs-markers it would generate.

INLINE COMMENTS

> martinvonz wrote in test-split.t:487-493
> Leaving these two behind seems reasonable. It would also be reasonable to evolve/stabilize them. Either way, it's different from what "hg rebase" does. Do we eventually want them to behave the same? If so, are we okay with a small BC break here (either in split or in rebase)?

I don't understand why you think "it's different from what "hg rebase" does". The `--rebase` flag does not suggest what rebase source or destination it uses. So it's up to the `split` implementation to do a reasonable thing. It can do `-r SMART_REVSET -d ...` instead of `-s SINGLE_REV -d ...`. I can revise the help text to clarify.

I think we wanted to implement "Option 2 (skip troublemakers)" as the default behavior in https://www.mercurial-scm.org/wiki/CEDRebase according to previous sprint discussion. That said, I don't think that should block this patch.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: lothiraldan, martinvonz, dlax, mercurial-devel
phabricator - Oct. 16, 2017, 6:56 p.m.
quark added inline comments.

INLINE COMMENTS

> martinvonz wrote in test-split.t:487-493
> Leaving these two behind seems reasonable. It would also be reasonable to evolve/stabilize them. Either way, it's different from what "hg rebase" does. Do we eventually want them to behave the same? If so, are we okay with a small BC break here (either in split or in rebase)?

>   It would also be reasonable to evolve/stabilize them. 

Only if we know that would not cause conflicts. Otherwise I don't think it's reasonable to do that automatically. This is also a difference between split's rebase and rebase command itself. The former cares about no-conflict experience and the latter does not care.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: lothiraldan, martinvonz, dlax, mercurial-devel
phabricator - Oct. 17, 2017, 8:02 p.m.
lothiraldan added a comment.


  In https://phab.mercurial-scm.org/D1082#18693, @quark wrote:
  
  > In https://phab.mercurial-scm.org/D1082#18648, @lothiraldan wrote:
  >
  > > It will be great to have split in core, even if it's only as an experimental experiment for now.
  > >
  > > I like the UX improvements, but could we add a config knob to disable the auto-rebase for power-users? I agree that generating orphans is maybe not the best UX for users, so I think having it on by default could be an improvement.
  >
  >
  > I believe most users want auto-rebase by default. Auto-rebase is the default at FB for months and people like it.
  
  
  Yes agreed, sorry if I was not clear, I think it's a good behavior for most users.
  
  > I agree power users may want a different default. In that case, you can set alias `split = split --no-rebase`.
  > 
  >> But, I often am in the middle of a too big stack and auto-rebasing would break my flow of fixing commits from bottom to top without mentioning the number of obs-markers it would generate.
  
  I'm still concerned about possible obsmarkers explosion as we move to more and more auto-stabilizing commands in core. For leaving a trace of the problem, if we use auto-stabilizing commands on every changeset on the stack (rewrite the first changeset, all parent get stabilized, rewrite the second changeset, all parents get stabilized, etc..), we will end up creating N²/2 obsmarkers with a stack of N:
  
    stack of 4 changeset: 8 obs-markers,
    stack of 10 changesets: 50 obs-markers,
    stack of 30 changesets: 450 obs-markers,
  
  I'm not saying that everyone has a 30 changesets stack but even with a small stack going back and forth in the stack while making editions would rapidly grow the number of obsmarkers created which will cause scalability issues when exchanging markers.
  
  There is a couple of interesting lead to solve this, maybe we could do something special when more than X (configurable) obs-markers are created. Probably avoid the stabilization and point to some documentation or commands (like restack).

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: lothiraldan, martinvonz, dlax, mercurial-devel
phabricator - Oct. 17, 2017, 8:57 p.m.
quark added a comment.


  In https://phab.mercurial-scm.org/D1082#19507, @lothiraldan wrote:
  
  > I'm still concerned about possible obsmarkers explosion as we move to more and more auto-stabilizing commands in core. For leaving a trace of the problem, if we use auto-stabilizing commands on every changeset on the stack (rewrite the first changeset, all parent get stabilized, rewrite the second changeset, all parents get stabilized, etc..), we will end up creating N²/2 obsmarkers with a stack of N:
  >
  >   stack of 4 changeset: 8 obs-markers,
  >   stack of 10 changesets: 50 obs-markers,
  >   stack of 30 changesets: 450 obs-markers,
  >   
  >
  > I'm not saying that everyone has a 30 changesets stack but even with a small stack going back and forth in the stack while making editions would rapidly grow the number of obsmarkers created which will cause scalability issues when exchanging markers.
  >
  > There is a couple of interesting lead to solve this, maybe we could do something special when more than X (configurable) obs-markers are created. Probably avoid the stabilization and point to some documentation or commands (like restack).
  
  
  In my opinion, the "obsmarkers explosion" problem is because the current algorithm loads the entire obsstore while it is not necessary to know all markers in all cases. i.e. "obsmarkers explosion" is NOT a problem if the algorithm is smarter that only parses or loads markers needed for certain calculations. i.e. `obsolete()` and all obsmarker-related revsets are lazy. Of course it takes some time to rewrite them into lazy versions.
  
  In additional, the `N^2` case you are talking about only happens if the user splits every commit in their draft stack, which is unrealistic. I don't think that should block the default behavior here.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: lothiraldan, martinvonz, dlax, mercurial-devel
phabricator - Dec. 15, 2017, 7:08 a.m.
martinvonz added a comment.


  I'd like to get this queued. Jun, can you rebase this and make sure tests still pass?

INLINE COMMENTS

> quark wrote in test-split.t:487-493
> >   It would also be reasonable to evolve/stabilize them. 
> 
> Only if we know that would not cause conflicts. Otherwise I don't think it's reasonable to do that automatically. This is also a difference between split's rebase and rebase command itself. The former cares about no-conflict experience and the latter does not care.

I agree now. This seems like a good default.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: lothiraldan, martinvonz, dlax, mercurial-devel
phabricator - Dec. 19, 2017, 10:21 p.m.
martinvonz added inline comments.

INLINE COMMENTS

> split.py:90
> +
> +        descendants = list(repo.revs('(%d::) - (%d)', ctx, ctx))
> +        alloworphaned = obsolete.isenabled(repo, obsolete.allowunstableopt)

s/ctx/rev/ seems a little clearer (I realize that ctx has a __int__ method, but I didn't know that until I read this)

> split.py:95
> +            # won't cause conflicts for sure.
> +            torebase = list(repo.revs('%ld - (%ld & obsolete())::', descendants,
> +                                      descendants))

Do we want to exclude all troubled commits? Maybe phase- and content-divergent ones are better to not rebase too? (I.e. '%ld - obsolete() - troubled()')

> split.py:98
> +            if not alloworphaned and len(torebase) != len(descendants):
> +                raise error.Abort(_('split will leave orphaned changesets '
> +                                    'behind'))

maybe s/will/would/ ?

> split.py:109
> +
> +        cmdutil.bailifchanged(repo, merge=False)
> +

Do we really want to allow splitting when there's an unfinished merge? Or is that handled elsewhere?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: lothiraldan, martinvonz, dlax, mercurial-devel
phabricator - Dec. 19, 2017, 10:48 p.m.
quark added inline comments.

INLINE COMMENTS

> martinvonz wrote in split.py:90
> s/ctx/rev/ seems a little clearer (I realize that ctx has a __int__ method, but I didn't know that until I read this)

Good advice. This is probably copy-pasted from old code.

> martinvonz wrote in split.py:95
> Do we want to exclude all troubled commits? Maybe phase- and content-divergent ones are better to not rebase too? (I.e. '%ld - obsolete() - troubled()')

I think it's better to rebase content-divergent changesets here. Since that enables people to check the final result of the divergence and be able to make a final judgement about which one to pick or whether to merge the divergence.

I also think in the future it might make sense to rebase some unstable ones too as long as there is a fast way to test if doing that won't cause merge conflicts.

> martinvonz wrote in split.py:98
> maybe s/will/would/ ?

Will change.

> martinvonz wrote in split.py:109
> Do we really want to allow splitting when there's an unfinished merge? Or is that handled elsewhere?

Good catch. This is unintentional. Will remove.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1082

To: quark, #hg-reviewers
Cc: lothiraldan, martinvonz, dlax, mercurial-devel

Patch

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,503 @@ 
+#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]
+  > stabilization=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]
+
+Split a head
+
+  $ $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
+
+  $ 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.stabilization=createmarkers
+  abort: split will 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,165 @@ 
+# split.py - split a changeset into smaller ones
+#
+# Copyright 2015 Laurent Charignon <lcharignon@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 (
+    short,
+)
+
+from mercurial import (
+    bookmarks,
+    cmdutil,
+    commands,
+    error,
+    extensions,
+    hg,
+    node,
+    obsolete,
+    registrar,
+    repair,
+    revsetlang,
+    scmutil,
+)
+
+from hgext 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'
+
+@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
+
+    Repetitively 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.
+
+    If --rebase was set, rebase old, connected, non-obsoleted descendants onto
+    the new changeset.
+    """
+    revlist = []
+    if opts.get('rev'):
+        revlist.append(opts.get('rev'))
+    revlist.extend(revs)
+    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() == node.nullid:
+        ui.status(_('nothing to split\n'))
+        return 1
+    if ctx.node() is None:
+        raise error.Abort(_('cannot split working directory'))
+
+    descendants = list(repo.revs('(%d::) - (%d)', ctx, ctx))
+    alloworphaned = obsolete.isenabled(repo, obsolete.allowunstableopt)
+    if opts.get('rebase'):
+        # skip obsoleted descendants and their descendants so the rebase won't
+        # cause conflicts.
+        torebase = list(repo.revs('%ld - (%ld & obsolete())::', descendants,
+                                  descendants))
+        if not alloworphaned and len(torebase) != len(descendants):
+            raise error.Abort(_('split will 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, merge=False)
+
+    # Deactivate bookmark temporarily so it won't get moved unintentionally
+    bname = repo._activebookmark
+    if bname and repo._bookmarks[bname] != ctx.node():
+        bookmarks.deactivate(repo)
+
+    with repo.wlock(), repo.lock(), repo.transaction('split') as tr:
+        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))