hgext/split.py
changeset 35455 02ea370c2baa
child 36400 7b86aa31b004
equal deleted inserted replaced
35454:786289423e97 35455:02ea370c2baa
       
     1 # split.py - split a changeset into smaller ones
       
     2 #
       
     3 # Copyright 2015 Laurent Charignon <lcharignon@fb.com>
       
     4 # Copyright 2017 Facebook, Inc.
       
     5 #
       
     6 # This software may be used and distributed according to the terms of the
       
     7 # GNU General Public License version 2 or any later version.
       
     8 """command to split a changeset into smaller ones (EXPERIMENTAL)"""
       
     9 
       
    10 from __future__ import absolute_import
       
    11 
       
    12 from mercurial.i18n import _
       
    13 
       
    14 from mercurial.node import (
       
    15     nullid,
       
    16     short,
       
    17 )
       
    18 
       
    19 from mercurial import (
       
    20     bookmarks,
       
    21     cmdutil,
       
    22     commands,
       
    23     error,
       
    24     hg,
       
    25     obsolete,
       
    26     phases,
       
    27     registrar,
       
    28     revsetlang,
       
    29     scmutil,
       
    30 )
       
    31 
       
    32 # allow people to use split without explicitly enabling rebase extension
       
    33 from . import (
       
    34     rebase,
       
    35 )
       
    36 
       
    37 cmdtable = {}
       
    38 command = registrar.command(cmdtable)
       
    39 
       
    40 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
       
    41 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
       
    42 # be specifying the version(s) of Mercurial they are tested with, or
       
    43 # leave the attribute unspecified.
       
    44 testedwith = 'ships-with-hg-core'
       
    45 
       
    46 @command('^split',
       
    47     [('r', 'rev', '', _("revision to split"), _('REV')),
       
    48      ('', 'rebase', True, _('rebase descendants after split')),
       
    49     ] + cmdutil.commitopts2,
       
    50     _('hg split [--no-rebase] [[-r] REV]'))
       
    51 def split(ui, repo, *revs, **opts):
       
    52     """split a changeset into smaller ones
       
    53 
       
    54     Repeatedly prompt changes and commit message for new changesets until there
       
    55     is nothing left in the original changeset.
       
    56 
       
    57     If --rev was not given, split the working directory parent.
       
    58 
       
    59     By default, rebase connected non-obsoleted descendants onto the new
       
    60     changeset. Use --no-rebase to avoid the rebase.
       
    61     """
       
    62     revlist = []
       
    63     if opts.get('rev'):
       
    64         revlist.append(opts.get('rev'))
       
    65     revlist.extend(revs)
       
    66     with repo.wlock(), repo.lock(), repo.transaction('split') as tr:
       
    67         revs = scmutil.revrange(repo, revlist or ['.'])
       
    68         if len(revs) > 1:
       
    69             raise error.Abort(_('cannot split multiple revisions'))
       
    70 
       
    71         rev = revs.first()
       
    72         ctx = repo[rev]
       
    73         if rev is None or ctx.node() == nullid:
       
    74             ui.status(_('nothing to split\n'))
       
    75             return 1
       
    76         if ctx.node() is None:
       
    77             raise error.Abort(_('cannot split working directory'))
       
    78 
       
    79         # rewriteutil.precheck is not very useful here because:
       
    80         # 1. null check is done above and it's more friendly to return 1
       
    81         #    instead of abort
       
    82         # 2. mergestate check is done below by cmdutil.bailifchanged
       
    83         # 3. unstable check is more complex here because of --rebase
       
    84         #
       
    85         # So only "public" check is useful and it's checked directly here.
       
    86         if ctx.phase() == phases.public:
       
    87             raise error.Abort(_('cannot split public changeset'),
       
    88                               hint=_("see 'hg help phases' for details"))
       
    89 
       
    90         descendants = list(repo.revs('(%d::) - (%d)', rev, rev))
       
    91         alloworphaned = obsolete.isenabled(repo, obsolete.allowunstableopt)
       
    92         if opts.get('rebase'):
       
    93             # Skip obsoleted descendants and their descendants so the rebase
       
    94             # won't cause conflicts for sure.
       
    95             torebase = list(repo.revs('%ld - (%ld & obsolete())::',
       
    96                                       descendants, descendants))
       
    97             if not alloworphaned and len(torebase) != len(descendants):
       
    98                 raise error.Abort(_('split would leave orphaned changesets '
       
    99                                     'behind'))
       
   100         else:
       
   101             if not alloworphaned and descendants:
       
   102                 raise error.Abort(
       
   103                     _('cannot split changeset with children without rebase'))
       
   104             torebase = ()
       
   105 
       
   106         if len(ctx.parents()) > 1:
       
   107             raise error.Abort(_('cannot split a merge changeset'))
       
   108 
       
   109         cmdutil.bailifchanged(repo)
       
   110 
       
   111         # Deactivate bookmark temporarily so it won't get moved unintentionally
       
   112         bname = repo._activebookmark
       
   113         if bname and repo._bookmarks[bname] != ctx.node():
       
   114             bookmarks.deactivate(repo)
       
   115 
       
   116         wnode = repo['.'].node()
       
   117         top = None
       
   118         try:
       
   119             top = dosplit(ui, repo, tr, ctx, opts)
       
   120         finally:
       
   121             # top is None: split failed, need update --clean recovery.
       
   122             # wnode == ctx.node(): wnode split, no need to update.
       
   123             if top is None or wnode != ctx.node():
       
   124                 hg.clean(repo, wnode, show_stats=False)
       
   125             if bname:
       
   126                 bookmarks.activate(repo, bname)
       
   127         if torebase and top:
       
   128             dorebase(ui, repo, torebase, top)
       
   129 
       
   130 def dosplit(ui, repo, tr, ctx, opts):
       
   131     committed = [] # [ctx]
       
   132 
       
   133     # Set working parent to ctx.p1(), and keep working copy as ctx's content
       
   134     # NOTE: if we can have "update without touching working copy" API, the
       
   135     # revert step could be cheaper.
       
   136     hg.clean(repo, ctx.p1().node(), show_stats=False)
       
   137     parents = repo.changelog.parents(ctx.node())
       
   138     ui.pushbuffer()
       
   139     cmdutil.revert(ui, repo, ctx, parents)
       
   140     ui.popbuffer() # discard "reverting ..." messages
       
   141 
       
   142     # Any modified, added, removed, deleted result means split is incomplete
       
   143     incomplete = lambda repo: any(repo.status()[:4])
       
   144 
       
   145     # Main split loop
       
   146     while incomplete(repo):
       
   147         if committed:
       
   148             header = (_('HG: Splitting %s. So far it has been split into:\n')
       
   149                       % short(ctx.node()))
       
   150             for c in committed:
       
   151                 firstline = c.description().split('\n', 1)[0]
       
   152                 header += _('HG: - %s: %s\n') % (short(c.node()), firstline)
       
   153             header += _('HG: Write commit message for the next split '
       
   154                         'changeset.\n')
       
   155         else:
       
   156             header = _('HG: Splitting %s. Write commit message for the '
       
   157                        'first split changeset.\n') % short(ctx.node())
       
   158         opts.update({
       
   159             'edit': True,
       
   160             'interactive': True,
       
   161             'message': header + ctx.description(),
       
   162         })
       
   163         commands.commit(ui, repo, **opts)
       
   164         newctx = repo['.']
       
   165         committed.append(newctx)
       
   166 
       
   167     if not committed:
       
   168         raise error.Abort(_('cannot split an empty revision'))
       
   169 
       
   170     scmutil.cleanupnodes(repo, {ctx.node(): [c.node() for c in committed]},
       
   171                          operation='split')
       
   172 
       
   173     return committed[-1]
       
   174 
       
   175 def dorebase(ui, repo, src, dest):
       
   176     rebase.rebase(ui, repo, rev=[revsetlang.formatspec('%ld', src)],
       
   177                   dest=revsetlang.formatspec('%d', dest))