Patchwork [1,of,2] split: new extension to split changesets

login
register
mail settings
Submitter Jun Wu
Date June 25, 2017, 5:45 a.m.
Message ID <c5c2d312b8293e948834.1498369501@x1c>
Download mbox | patch
Permalink /patch/21695/
State Superseded
Headers show

Comments

Jun Wu - June 25, 2017, 5:45 a.m.
# HG changeset patch
# User Jun Wu <quark@fb.com>
# Date 1498360490 25200
#      Sat Jun 24 20:14:50 2017 -0700
# Node ID c5c2d312b8293e9488344b39c0889faa3c4442eb
# Parent  bec821f3bb744a7d13e33436433005654a263bc3
# Available At https://bitbucket.org/quark-zju/hg-draft
#              hg pull https://bitbucket.org/quark-zju/hg-draft -r c5c2d312b829
split: new extension to split changesets

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.
    It also enables splitting a non-head commit for repos without obsstore,
    since there won't be unstable changesets before and after the single
    transaction.
    This has been on by default at Facebook for months now and seems to be a
    good UX improvement.

  - Remove "Done split? [y/n]" prompt.
    That could be detected by checking repo.status() instead.

[1]: https://bitbucket.org/marmoute/mutable-history/commits/9603aa1ecdfd54b
via Mercurial-devel - June 25, 2017, 7:26 a.m.
On Sat, Jun 24, 2017 at 10:45 PM, Jun Wu <quark@fb.com> wrote:
> # HG changeset patch
> # User Jun Wu <quark@fb.com>
> # Date 1498360490 25200
> #      Sat Jun 24 20:14:50 2017 -0700
> # Node ID c5c2d312b8293e9488344b39c0889faa3c4442eb
> # Parent  bec821f3bb744a7d13e33436433005654a263bc3
> # Available At https://bitbucket.org/quark-zju/hg-draft
> #              hg pull https://bitbucket.org/quark-zju/hg-draft -r c5c2d312b829
> split: new extension to split changesets
>
> 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.
>     It also enables splitting a non-head commit for repos without obsstore,
>     since there won't be unstable changesets before and after the single
>     transaction.
>     This has been on by default at Facebook for months now and seems to be a
>     good UX improvement.
>
>   - Remove "Done split? [y/n]" prompt.
>     That could be detected by checking repo.status() instead.
>
> [1]: https://bitbucket.org/marmoute/mutable-history/commits/9603aa1ecdfd54b
>
> diff --git a/hgext/split.py b/hgext/split.py
> new file mode 100644
> --- /dev/null
> +++ b/hgext/split.py
> @@ -0,0 +1,168 @@
> +# 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 import (
> +    bookmarks,
> +    cmdutil,
> +    commands,
> +    error,
> +    extensions,
> +    hg,
> +    node,
> +    obsolete,
> +    registrar,
> +    revsetlang,
> +    scmutil,
> +)
> +
> +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')),
> +     ('', 'no-rebase', False, _('do not rebase descendants after split')),

Will --rebase be allowed? Or will it be --no-no-rebase?

> +    ] + cmdutil.commitopts2,
> +    _('hg split [[-r] REV] [--no-rebase]'))
> +def split(ui, repo, *args, **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 --no-rebase was not set, rebase old descendants on top of the last new
> +    changeset.
> +    """
> +    revlist = []
> +    if opts.get('rev'):
> +        revlist.append(opts.get('rev'))

I think you can change the default from '' to [] and make these 3
lines just "revlist = opts['rev']".

> +    revlist.extend(args)
> +    revs = scmutil.revrange(repo, revlist or ['.'])
> +    if len(revs) > 1:

Should also handle "len(revs) == 0" here, either separately or
combined with this case.

> +        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'))

Should we also error out if if we're trying split a merge?

> +
> +    descendants = repo.revs('(%d::) - (%d)', ctx, ctx)

Do we care about the race between this check and the locking of the
repo? There's a risk that another descendant is created before we lock
the repo. If that happens without obsmarkers, I'm guessing the rebase
will fail (after the splitting is done, so would be a little
annoying). If it happens with obsmarkers, I'm guessing there will be
an unstable commit on top (which seems fine, but against the sprit of
the "rebase by default" you have chosen).

> +    if opts.get('no_rebase'):
> +        allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
> +        if not allowunstable and descendants:
> +            raise error.Abort(
> +                _('cannot split changeset with children without rebase'))
> +        torebase = ()
> +    else:
> +        if descendants:
> +            try:
> +                extensions.find('rebase').rebase
> +            except (KeyError, AttributeError):
> +                raise error.Abort(_('rebase extension is required'))
> +        torebase = descendants
> +
> +    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():
> +        with repo.lock():
> +            # Not using transaction with block because "dorebase" may want to
> +            # close the transaction earlier.
> +            # NOTE: strip might be made smarter to "work" in a transaction.
> +            tr = repo.transaction('split')
> +            try:
> +                wnode = repo['.'].node()
> +                top = None
> +                try:
> +                    top = dosplit(ui, repo, tr, ctx, opts)
> +                finally:
> +                    # top is None: split failed, need update --clean recovery.

Why not handle this with an "except <some exception>:"? Oh, because
you want to call hg.clean() in both cases?

> +                    # 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, tr, torebase, top)

It might be clearer to move the first 3 lines of dorebase() to here to
have all the transaction opening and closing in one place. Then you
can remove the comment above.

> +            finally:
> +                if tr.running():
> +                    tr.close()
> +
> +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):
> +        opts.update({
> +            'edit': True,
> +            'interactive': True,
> +            'message': ctx.description(),
> +        })
> +        commands.commit(ui, repo, **opts)
> +        newctx = repo['.']
> +        committed.append(newctx)
> +
> +    if not committed:
> +        raise error.Abort(_('cannot split an empty revision'))
> +
> +    # Move bookmarks
> +    oldbookmarks = repo.nodebookmarks(ctx.node())
> +    if oldbookmarks and committed:
> +        marks = repo._bookmarks
> +        newnode = committed[-1].node()
> +        for name in oldbookmarks:
> +            marks[name] = newnode
> +        marks.recordchange(tr)
> +
> +    # Write obsmarkers
> +    if obsolete.isenabled(repo, obsolete.createmarkersopt):
> +        obsolete.createmarkers(repo, [(ctx, committed)], operation='split')
> +
> +    return committed[-1]
> +
> +def dorebase(ui, repo, tr, src, dest):
> +    if not obsolete.isenabled(repo, obsolete.createmarkersopt) and tr:
> +        # rebase calls strip, which cannot be inside a transaction
> +        tr.close()
> +    rebase = extensions.find('rebase')
> +    rebase.rebase(ui, repo, rev=[revsetlang.formatspec('%ld', src)],
> +                  dest=revsetlang.formatspec('%d', dest))
> 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,437 @@
> +#testcases default obsstore
> +
> +  $ 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]
> +  > split=
> +  > [ui]
> +  > interactive=1
> +  > [diff]
> +  > git=1
> +  > unified=0
> +  > [alias]
> +  > glog=log -G -T '{rev}:{node|short} {desc} {bookmarks}\n'
> +  > EOF
> +
> +#if obsstore
> +  $ 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]
> +
> +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
> +
> +  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: 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: 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: 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
> +
> +  $ hg bookmark
> +     r1                        0:a61bcde8c529
> +     r2                        4:00eebaf8d2e2
> +   * r3                        4:00eebaf8d2e2
> +#if default
> +  $ hg glog -p
> +  @  4:00eebaf8d2e2 split 3 r2 r3
> +  |  diff --git a/a b/a
> +  |  --- a/a
> +  |  +++ b/a
> +  |  @@ -1,1 +1,1 @@
> +  |  -1
> +  |  +11
> +  |
> +  o  3:a09ad58faae3 split 2
> +  |  diff --git a/a b/a
> +  |  --- a/a
> +  |  +++ b/a
> +  |  @@ -3,1 +3,1 @@
> +  |  -3
> +  |  +33
> +  |
> +  o  2:e704349bd21b split 1
> +  |  diff --git a/a b/a
> +  |  --- a/a
> +  |  +++ b/a
> +  |  @@ -5,1 +5,1 @@
> +  |  -5
> +  |  +55
> +  |
> +  | o  1:1df0d5c5a3ab a2

Why is a2 still here? I would expect it to get stripped.

> +  |/   diff --git a/a b/a
> +  |    --- a/a
> +  |    +++ b/a
> +  |    @@ -1,1 +1,1 @@
> +  |    -1
> +  |    +11
> +  |    @@ -3,1 +3,1 @@
> +  |    -3
> +  |    +33
> +  |    @@ -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
> +
> +#endif
> +#if obsstore
> +  $ 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
> +
> +  $ hg bookmark
> +     r1                        0:a61bcde8c529
> +     r2                        4:00eebaf8d2e2
> +   * r3                        0:a61bcde8c529
> +
> +#if default
> +  $ hg glog
> +  o  4:00eebaf8d2e2 split 3 r2
> +  |
> +  o  3:a09ad58faae3 split 2
> +  |
> +  o  2:e704349bd21b split 1
> +  |
> +  | o  1:1df0d5c5a3ab a2
> +  |/
> +  @  0:a61bcde8c529 a1 r1 r3
> +
> +#endif
> +#if obsstore
> +  $ 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

Could you add another test for "Split a non-head with obsolete
descendant"? I'm curious what the behavior would be. If the descendant
had a successor, would we get divergence through the rebase?

> +
> +  $ 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 1
> +  abort: rebase extension is required
> +  [255]
> +  $ runsplit -r 1 --config extensions.rebase= | grep rebasing
> +  rebasing 2:b5c5ea414030 "d1" (d1)
> +  rebasing 3:f4a0a8d004cc "d2" (d2)
> +  rebasing 4:777940761eba "d3" (d3)
> +#if default
> +  $ hg bookmark
> +     d1                        5:c4b449ef030e
> +   * d2                        6:c9dd00ab36a3
> +     d3                        7:19f476bc865c
> +     r1                        0:a61bcde8c529
> +     r2                        4:00eebaf8d2e2
> +  $ hg glog -p
> +  o  7:19f476bc865c d3 d3
> +  |  diff --git a/d b/d
> +  |  --- a/d
> +  |  +++ b/d
> +  |  @@ -2,0 +3,1 @@
> +  |  +3
> +  |
> +  @  6:c9dd00ab36a3 d2 d2
> +  |  diff --git a/d b/d
> +  |  --- a/d
> +  |  +++ b/d
> +  |  @@ -1,0 +2,1 @@
> +  |  +2
> +  |
> +  o  5:c4b449ef030e d1 d1
> +  |  diff --git a/d b/d
> +  |  new file mode 100644
> +  |  --- /dev/null
> +  |  +++ b/d
> +  |  @@ -0,0 +1,1 @@
> +  |  +d
> +  |
> +  o  4:00eebaf8d2e2 split 3 r2
> +  |  diff --git a/a b/a
> +  |  --- a/a
> +  |  +++ b/a
> +  |  @@ -1,1 +1,1 @@
> +  |  -1
> +  |  +11
> +  |
> +  o  3:a09ad58faae3 split 2
> +  |  diff --git a/a b/a
> +  |  --- a/a
> +  |  +++ b/a
> +  |  @@ -3,1 +3,1 @@
> +  |  -3
> +  |  +33
> +  |
> +  o  2:e704349bd21b split 1
> +  |  diff --git a/a b/a
> +  |  --- a/a
> +  |  +++ b/a
> +  |  @@ -5,1 +5,1 @@
> +  |  -5
> +  |  +55
> +  |
> +  | o  1:1df0d5c5a3ab a2
> +  |/   diff --git a/a b/a
> +  |    --- a/a
> +  |    +++ b/a
> +  |    @@ -1,1 +1,1 @@
> +  |    -1
> +  |    +11
> +  |    @@ -3,1 +3,1 @@
> +  |    -3
> +  |    +33
> +  |    @@ -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
> +
> +#endif
> +#if obsstore
> +  $ 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 default
> +  $ runsplit -r 1 --no-rebase
> +  abort: cannot split changeset with children without rebase
> +  [255]
> +#endif
> +#if obsstore
> +  $ 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
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Jun Wu - June 25, 2017, 4:12 p.m.
Thanks for prompt review! I'll wait for more comments before sending V3 on
Tuesday.

Excerpts from Martin von Zweigbergk's message of 2017-06-25 00:26:32 -0700:
> > +@command('^split',
> > +    [('r', 'rev', '', _("revision to split"), _('REV')),
> > +     ('', 'no-rebase', False, _('do not rebase descendants after split')),
> 
> Will --rebase be allowed? Or will it be --no-no-rebase?

I think that's more of fancyopts's job - it needs to detect --no prefix and
do fancy things (like, --rebase is a no-op, --rebase --no-rebase is an
error).

> > +    revlist = []
> > +    if opts.get('rev'):
> > +        revlist.append(opts.get('rev'))
> 
> I think you can change the default from '' to [] and make these 3
> lines just "revlist = opts['rev']".

This affects help text. Compare "hg log -h" with "hg update -h", the latter
accepts one revs and does not have a "[+]".

> > +    revlist.extend(args)
> > +    revs = scmutil.revrange(repo, revlist or ['.'])
> > +    if len(revs) > 1:
> 
> Should also handle "len(revs) == 0" here, either separately or
> combined with this case.

If len(revs) == 0, revs.first() returns None, and it's "nothing to split".
I'll add a comment.

> > +    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'))
> 
> Should we also error out if if we're trying split a merge?

Yes. It's taken care in below code.  I'll add a test case for merge.

> > +
> > +    descendants = repo.revs('(%d::) - (%d)', ctx, ctx)
> 
> Do we care about the race between this check and the locking of the
> repo? There's a risk that another descendant is created before we lock
> the repo. If that happens without obsmarkers, I'm guessing the rebase
> will fail (after the splitting is done, so would be a little
> annoying). If it happens with obsmarkers, I'm guessing there will be
> an unstable commit on top (which seems fine, but against the sprit of
> the "rebase by default" you have chosen).

Good catch! I'll move the check inside the lock.

> > +    with repo.wlock():
> > +        with repo.lock():
> > +            # Not using transaction with block because "dorebase" may want to
> > +            # close the transaction earlier.
> > +            # NOTE: strip might be made smarter to "work" in a transaction.
> > +            tr = repo.transaction('split')
> > +            try:
> > +                wnode = repo['.'].node()
> > +                top = None
> > +                try:
> > +                    top = dosplit(ui, repo, tr, ctx, opts)
> > +                finally:
> > +                    # top is None: split failed, need update --clean recovery.
> 
> Why not handle this with an "except <some exception>:"? Oh, because
> you want to call hg.clean() in both cases?

Yes, the "successful" case also needs an update --clean since the working
copy is made dirty by split (to be able to run commit -i).

> > +                    # 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, tr, torebase, top)
> 
> It might be clearer to move the first 3 lines of dorebase() to here to
> have all the transaction opening and closing in one place. Then you
> can remove the comment above.

I think "strip" requiring no transaction is the root cause that the code
cannot be clean here (and in some other places). I'll try to modify strip
code so it "works" inside a transaction.
via Mercurial-devel - June 26, 2017, 4:47 p.m.
On Sat, Jun 24, 2017 at 10:45 PM, Jun Wu <quark@fb.com> wrote:
> +@command('^split',
> +    [('r', 'rev', '', _("revision to split"), _('REV')),
> +     ('', 'no-rebase', False, _('do not rebase descendants after split')),
> +    ] + cmdutil.commitopts2,

The default of False probably deserves a separate discussion.

When allowunstable is not enabled, I think other commands ("commit
--amend", "rebase", "histedit") currently simply fail if there are
descendants of the rewritten commit, so we could make them rebase
instead without breaking BC too much (not error out anymore is usually
fine).

When allowunstable *is* enabled, none of "hg commit --amend", "hg
rebase", and "hg histedit" currently rebase by default. Unless we
decide that we should change their defaults, I don't see why the
default of "hg split" should be different. It's probably okay to
change the default of those commands given that evolve is still
experimental? (Although evolve is not required for using
allowunstable, I'd be surprised if users set allowunstable without
evolve enabled.)
Jun Wu - June 26, 2017, 5:59 p.m.
Excerpts from Martin von Zweigbergk's message of 2017-06-26 09:47:22 -0700:
> On Sat, Jun 24, 2017 at 10:45 PM, Jun Wu <quark@fb.com> wrote:
> > +@command('^split',
> > +    [('r', 'rev', '', _("revision to split"), _('REV')),
> > +     ('', 'no-rebase', False, _('do not rebase descendants after split')),
> > +    ] + cmdutil.commitopts2,
> 
> The default of False probably deserves a separate discussion.
>
> When allowunstable is not enabled, I think other commands ("commit
> --amend", "rebase", "histedit") currently simply fail if there are
> descendants of the rewritten commit, so we could make them rebase
> instead without breaking BC too much (not error out anymore is usually
> fine).

The "allowunstable" config literally reads "whether unstable changesets are
allowed or not", it's not directly related to "rebase" and should not affect
the flag here.
 
> When allowunstable *is* enabled, none of "hg commit --amend", "hg
> rebase", and "hg histedit" currently rebase by default. Unless we
> decide that we should change their defaults, I don't see why the
> default of "hg split" should be different. It's probably okay to
> change the default of those commands given that evolve is still
> experimental? (Although evolve is not required for using
> allowunstable, I'd be surprised if users set allowunstable without
> evolve enabled.)

I think one key difference between amend and split is, amend may introduce
conflicts if rebased, so it makes sense to not rebase by default. A user
may want to amend again and again until they feel comfortable to do the
rebase and face the conflicts.

"split" will never cause rebase conflicts since the last new changeset
should have exactly the same working copy content as the original changeset.
So it's more friendly to rebase by default. (same for part of histedit)

At Facebook, amend won't do an automatic rebase by default but split, fold
or metaedit will.

Since split, fold, metaedit are new commands, and amend is better not doing
a rebase, I think we don't have to BC anything.
via Mercurial-devel - June 26, 2017, 6:08 p.m.
On Mon, Jun 26, 2017 at 10:59 AM, Jun Wu <quark@fb.com> wrote:
> Excerpts from Martin von Zweigbergk's message of 2017-06-26 09:47:22 -0700:
>> On Sat, Jun 24, 2017 at 10:45 PM, Jun Wu <quark@fb.com> wrote:
>> > +@command('^split',
>> > +    [('r', 'rev', '', _("revision to split"), _('REV')),
>> > +     ('', 'no-rebase', False, _('do not rebase descendants after split')),
>> > +    ] + cmdutil.commitopts2,
>>
>> The default of False probably deserves a separate discussion.
>>
>> When allowunstable is not enabled, I think other commands ("commit
>> --amend", "rebase", "histedit") currently simply fail if there are
>> descendants of the rewritten commit, so we could make them rebase
>> instead without breaking BC too much (not error out anymore is usually
>> fine).
>
> The "allowunstable" config literally reads "whether unstable changesets are
> allowed or not", it's not directly related to "rebase" and should not affect
> the flag here.
>
>> When allowunstable *is* enabled, none of "hg commit --amend", "hg
>> rebase", and "hg histedit" currently rebase by default. Unless we
>> decide that we should change their defaults, I don't see why the
>> default of "hg split" should be different. It's probably okay to
>> change the default of those commands given that evolve is still
>> experimental? (Although evolve is not required for using
>> allowunstable, I'd be surprised if users set allowunstable without
>> evolve enabled.)
>
> I think one key difference between amend and split is, amend may introduce
> conflicts if rebased, so it makes sense to not rebase by default. A user
> may want to amend again and again until they feel comfortable to do the
> rebase and face the conflicts.
>
> "split" will never cause rebase conflicts since the last new changeset
> should have exactly the same working copy content as the original changeset.
> So it's more friendly to rebase by default. (same for part of histedit)
>
> At Facebook, amend won't do an automatic rebase by default but split, fold
> or metaedit will.
>
> Since split, fold, metaedit are new commands, and amend is better not doing
> a rebase, I think we don't have to BC anything.

That makes sense.

My question about divergence still stands, I think.
Jun Wu - June 26, 2017, 6:46 p.m.
Excerpts from Martin von Zweigbergk's message of 2017-06-26 11:08:51 -0700:
> That makes sense.
> 
> My question about divergence still stands, I think.

To be clear, "divergence" is when rebase would create divergence changesets.

I think we can implement and have some [commands] config so users could
choose their desired behavior listed in CEDRebase wiki [1].

I agree if split (with rebase enabled) is nicer to abort early if the rebase
will abort. But the implementation is cleaner if split is not coupled with
rebase internal. Since the divergent case is pretty advanced and we do have
backups for split selection, I'd prefer cleaner implementation.

[1]: https://www.mercurial-scm.org/wiki/CEDRebase
Jun Wu - June 28, 2017, 12:21 a.m.
Excerpts from Martin von Zweigbergk's message of 2017-06-25 00:26:32 -0700:
> Why is a2 still here? I would expect it to get stripped.

Sorry, I missed this one (I probably want to do some email client changes to
make sure I won't miss comments).

It's a bug and fixed in V2.

> Could you add another test for "Split a non-head with obsolete
> descendant"? I'm curious what the behavior would be. If the descendant
> had a successor, would we get divergence through the rebase?

That feels like rebase's responsible to do it right. I'll try to fix
rebase's code instead. But that's depending on a lot of things already so
I'll probably defer sending split patches until rebase is good enough to
use.
via Mercurial-devel - June 28, 2017, 3:51 a.m.
On Tue, Jun 27, 2017 at 5:21 PM, Jun Wu <quark@fb.com> wrote:
> Excerpts from Martin von Zweigbergk's message of 2017-06-25 00:26:32 -0700:
>> Why is a2 still here? I would expect it to get stripped.
>
> Sorry, I missed this one (I probably want to do some email client changes to
> make sure I won't miss comments).
>
> It's a bug and fixed in V2.
>
>> Could you add another test for "Split a non-head with obsolete
>> descendant"? I'm curious what the behavior would be. If the descendant
>> had a successor, would we get divergence through the rebase?
>
> That feels like rebase's responsible to do it right. I'll try to fix
> rebase's code instead. But that's depending on a lot of things already so
> I'll probably defer sending split patches until rebase is good enough to
> use.

I agree that it's a problem that rebase can deal better with. However,
since it doesn't yet do that, I think it becomes split's problem. We
have the config to allow divergence. If that's not set, I suppose the
rebase will fail. Split should ideally detect that early so the user
won't have lost their work deciding how to split the commit up.
However, I don't think that feature needs to go into the first version
of split. We just need to get it fixed before the next release.
Jun Wu - June 28, 2017, 4:08 a.m.
Excerpts from Martin von Zweigbergk's message of 2017-06-27 20:51:48 -0700:
> I agree that it's a problem that rebase can deal better with. However,
> since it doesn't yet do that, I think it becomes split's problem. We
> have the config to allow divergence. If that's not set, I suppose the
> rebase will fail. Split should ideally detect that early so the user
> won't have lost their work deciding how to split the commit up.
> However, I don't think that feature needs to go into the first version
> of split. We just need to get it fixed before the next release.

I have another idea - having a "splitstate" so people can resume a split.
That solves other issue like if people exit the editor with non zero code,
they won't lose previously split commits. It makes the split implementation
more complex, but I think I can deal with that complexity and it looks
cleaner. What do you think?
via Mercurial-devel - June 28, 2017, 4:09 a.m.
On Tue, Jun 27, 2017 at 9:08 PM, Jun Wu <quark@fb.com> wrote:
> Excerpts from Martin von Zweigbergk's message of 2017-06-27 20:51:48 -0700:
>> I agree that it's a problem that rebase can deal better with. However,
>> since it doesn't yet do that, I think it becomes split's problem. We
>> have the config to allow divergence. If that's not set, I suppose the
>> rebase will fail. Split should ideally detect that early so the user
>> won't have lost their work deciding how to split the commit up.
>> However, I don't think that feature needs to go into the first version
>> of split. We just need to get it fixed before the next release.
>
> I have another idea - having a "splitstate" so people can resume a split.
> That solves other issue like if people exit the editor with non zero code,
> they won't lose previously split commits. It makes the split implementation
> more complex, but I think I can deal with that complexity and it looks
> cleaner. What do you think?

I agree. I had that idea too, but it sounded like more work that just
aborting, and I didn't want to ask for more work. But if you don't
mind, go for it :-)

Patch

diff --git a/hgext/split.py b/hgext/split.py
new file mode 100644
--- /dev/null
+++ b/hgext/split.py
@@ -0,0 +1,168 @@ 
+# 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 import (
+    bookmarks,
+    cmdutil,
+    commands,
+    error,
+    extensions,
+    hg,
+    node,
+    obsolete,
+    registrar,
+    revsetlang,
+    scmutil,
+)
+
+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')),
+     ('', 'no-rebase', False, _('do not rebase descendants after split')),
+    ] + cmdutil.commitopts2,
+    _('hg split [[-r] REV] [--no-rebase]'))
+def split(ui, repo, *args, **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 --no-rebase was not set, rebase old descendants on top of the last new
+    changeset.
+    """
+    revlist = []
+    if opts.get('rev'):
+        revlist.append(opts.get('rev'))
+    revlist.extend(args)
+    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 = repo.revs('(%d::) - (%d)', ctx, ctx)
+    if opts.get('no_rebase'):
+        allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
+        if not allowunstable and descendants:
+            raise error.Abort(
+                _('cannot split changeset with children without rebase'))
+        torebase = ()
+    else:
+        if descendants:
+            try:
+                extensions.find('rebase').rebase
+            except (KeyError, AttributeError):
+                raise error.Abort(_('rebase extension is required'))
+        torebase = descendants
+
+    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():
+        with repo.lock():
+            # Not using transaction with block because "dorebase" may want to
+            # close the transaction earlier.
+            # NOTE: strip might be made smarter to "work" in a transaction.
+            tr = repo.transaction('split')
+            try:
+                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, tr, torebase, top)
+            finally:
+                if tr.running():
+                    tr.close()
+
+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):
+        opts.update({
+            'edit': True,
+            'interactive': True,
+            'message': ctx.description(),
+        })
+        commands.commit(ui, repo, **opts)
+        newctx = repo['.']
+        committed.append(newctx)
+
+    if not committed:
+        raise error.Abort(_('cannot split an empty revision'))
+
+    # Move bookmarks
+    oldbookmarks = repo.nodebookmarks(ctx.node())
+    if oldbookmarks and committed:
+        marks = repo._bookmarks
+        newnode = committed[-1].node()
+        for name in oldbookmarks:
+            marks[name] = newnode
+        marks.recordchange(tr)
+
+    # Write obsmarkers
+    if obsolete.isenabled(repo, obsolete.createmarkersopt):
+        obsolete.createmarkers(repo, [(ctx, committed)], operation='split')
+
+    return committed[-1]
+
+def dorebase(ui, repo, tr, src, dest):
+    if not obsolete.isenabled(repo, obsolete.createmarkersopt) and tr:
+        # rebase calls strip, which cannot be inside a transaction
+        tr.close()
+    rebase = extensions.find('rebase')
+    rebase.rebase(ui, repo, rev=[revsetlang.formatspec('%ld', src)],
+                  dest=revsetlang.formatspec('%d', dest))
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,437 @@ 
+#testcases default obsstore
+
+  $ 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]
+  > split=
+  > [ui]
+  > interactive=1
+  > [diff]
+  > git=1
+  > unified=0
+  > [alias]
+  > glog=log -G -T '{rev}:{node|short} {desc} {bookmarks}\n'
+  > EOF
+
+#if obsstore
+  $ 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]
+
+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
+  
+  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: 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: 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: 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
+
+  $ hg bookmark
+     r1                        0:a61bcde8c529
+     r2                        4:00eebaf8d2e2
+   * r3                        4:00eebaf8d2e2
+#if default
+  $ hg glog -p
+  @  4:00eebaf8d2e2 split 3 r2 r3
+  |  diff --git a/a b/a
+  |  --- a/a
+  |  +++ b/a
+  |  @@ -1,1 +1,1 @@
+  |  -1
+  |  +11
+  |
+  o  3:a09ad58faae3 split 2
+  |  diff --git a/a b/a
+  |  --- a/a
+  |  +++ b/a
+  |  @@ -3,1 +3,1 @@
+  |  -3
+  |  +33
+  |
+  o  2:e704349bd21b split 1
+  |  diff --git a/a b/a
+  |  --- a/a
+  |  +++ b/a
+  |  @@ -5,1 +5,1 @@
+  |  -5
+  |  +55
+  |
+  | o  1:1df0d5c5a3ab a2
+  |/   diff --git a/a b/a
+  |    --- a/a
+  |    +++ b/a
+  |    @@ -1,1 +1,1 @@
+  |    -1
+  |    +11
+  |    @@ -3,1 +3,1 @@
+  |    -3
+  |    +33
+  |    @@ -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
+  
+#endif
+#if obsstore
+  $ 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
+
+  $ hg bookmark
+     r1                        0:a61bcde8c529
+     r2                        4:00eebaf8d2e2
+   * r3                        0:a61bcde8c529
+
+#if default
+  $ hg glog
+  o  4:00eebaf8d2e2 split 3 r2
+  |
+  o  3:a09ad58faae3 split 2
+  |
+  o  2:e704349bd21b split 1
+  |
+  | o  1:1df0d5c5a3ab a2
+  |/
+  @  0:a61bcde8c529 a1 r1 r3
+  
+#endif
+#if obsstore
+  $ 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 1
+  abort: rebase extension is required
+  [255]
+  $ runsplit -r 1 --config extensions.rebase= | grep rebasing
+  rebasing 2:b5c5ea414030 "d1" (d1)
+  rebasing 3:f4a0a8d004cc "d2" (d2)
+  rebasing 4:777940761eba "d3" (d3)
+#if default
+  $ hg bookmark
+     d1                        5:c4b449ef030e
+   * d2                        6:c9dd00ab36a3
+     d3                        7:19f476bc865c
+     r1                        0:a61bcde8c529
+     r2                        4:00eebaf8d2e2
+  $ hg glog -p
+  o  7:19f476bc865c d3 d3
+  |  diff --git a/d b/d
+  |  --- a/d
+  |  +++ b/d
+  |  @@ -2,0 +3,1 @@
+  |  +3
+  |
+  @  6:c9dd00ab36a3 d2 d2
+  |  diff --git a/d b/d
+  |  --- a/d
+  |  +++ b/d
+  |  @@ -1,0 +2,1 @@
+  |  +2
+  |
+  o  5:c4b449ef030e d1 d1
+  |  diff --git a/d b/d
+  |  new file mode 100644
+  |  --- /dev/null
+  |  +++ b/d
+  |  @@ -0,0 +1,1 @@
+  |  +d
+  |
+  o  4:00eebaf8d2e2 split 3 r2
+  |  diff --git a/a b/a
+  |  --- a/a
+  |  +++ b/a
+  |  @@ -1,1 +1,1 @@
+  |  -1
+  |  +11
+  |
+  o  3:a09ad58faae3 split 2
+  |  diff --git a/a b/a
+  |  --- a/a
+  |  +++ b/a
+  |  @@ -3,1 +3,1 @@
+  |  -3
+  |  +33
+  |
+  o  2:e704349bd21b split 1
+  |  diff --git a/a b/a
+  |  --- a/a
+  |  +++ b/a
+  |  @@ -5,1 +5,1 @@
+  |  -5
+  |  +55
+  |
+  | o  1:1df0d5c5a3ab a2
+  |/   diff --git a/a b/a
+  |    --- a/a
+  |    +++ b/a
+  |    @@ -1,1 +1,1 @@
+  |    -1
+  |    +11
+  |    @@ -3,1 +3,1 @@
+  |    -3
+  |    +33
+  |    @@ -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
+  
+#endif
+#if obsstore
+  $ 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 default
+  $ runsplit -r 1 --no-rebase
+  abort: cannot split changeset with children without rebase
+  [255]
+#endif
+#if obsstore
+  $ 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