Mercurial > hg
comparison hgext/split.py @ 35455:02ea370c2baa
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.
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
Differential Revision: https://phab.mercurial-scm.org/D1082
author | Jun Wu <quark@fb.com> |
---|---|
date | Sat, 24 Jun 2017 23:03:41 -0700 |
parents | |
children | 7b86aa31b004 |
comparison
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)) |