35459
|
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))
|