##// END OF EJS Templates
split: new extension to split changesets...
Jun Wu -
r35471:02ea370c @7 default
parent child Browse files
Show More
@@ -0,0 +1,177 b''
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))
This diff has been collapsed as it changes many lines, (525 lines changed) Show them Hide them
@@ -0,0 +1,525 b''
1 #testcases obsstore-on obsstore-off
2
3 $ cat > $TESTTMP/editor.py <<EOF
4 > #!$PYTHON
5 > import os, sys
6 > path = os.path.join(os.environ['TESTTMP'], 'messages')
7 > messages = open(path).read().split('--\n')
8 > prompt = open(sys.argv[1]).read()
9 > sys.stdout.write(''.join('EDITOR: %s' % l for l in prompt.splitlines(True)))
10 > sys.stdout.flush()
11 > with open(sys.argv[1], 'w') as f:
12 > f.write(messages[0])
13 > with open(path, 'w') as f:
14 > f.write('--\n'.join(messages[1:]))
15 > EOF
16
17 $ cat >> $HGRCPATH <<EOF
18 > [extensions]
19 > drawdag=$TESTDIR/drawdag.py
20 > split=
21 > [ui]
22 > interactive=1
23 > [diff]
24 > git=1
25 > unified=0
26 > [alias]
27 > glog=log -G -T '{rev}:{node|short} {desc} {bookmarks}\n'
28 > EOF
29
30 #if obsstore-on
31 $ cat >> $HGRCPATH <<EOF
32 > [experimental]
33 > evolution=all
34 > EOF
35 #endif
36
37 $ hg init a
38 $ cd a
39
40 Nothing to split
41
42 $ hg split
43 nothing to split
44 [1]
45
46 $ hg commit -m empty --config ui.allowemptycommit=1
47 $ hg split
48 abort: cannot split an empty revision
49 [255]
50
51 $ rm -rf .hg
52 $ hg init
53
54 Cannot split working directory
55
56 $ hg split -r 'wdir()'
57 abort: cannot split working directory
58 [255]
59
60 Generate some content
61
62 $ $TESTDIR/seq.py 1 5 >> a
63 $ hg ci -m a1 -A a -q
64 $ hg bookmark -i r1
65 $ sed 's/1/11/;s/3/33/;s/5/55/' a > b
66 $ mv b a
67 $ hg ci -m a2 -q
68 $ hg bookmark -i r2
69
70 Cannot split a public changeset
71
72 $ hg phase --public -r 'all()'
73 $ hg split .
74 abort: cannot split public changeset
75 (see 'hg help phases' for details)
76 [255]
77
78 $ hg phase --draft -f -r 'all()'
79
80 Cannot split while working directory is dirty
81
82 $ touch dirty
83 $ hg add dirty
84 $ hg split .
85 abort: uncommitted changes
86 [255]
87 $ hg forget dirty
88 $ rm dirty
89
90 Split a head
91
92 $ cp -R . ../b
93 $ cp -R . ../c
94
95 $ hg bookmark r3
96
97 $ hg split 'all()'
98 abort: cannot split multiple revisions
99 [255]
100
101 $ runsplit() {
102 > cat > $TESTTMP/messages <<EOF
103 > split 1
104 > --
105 > split 2
106 > --
107 > split 3
108 > EOF
109 > cat <<EOF | hg split "$@"
110 > y
111 > y
112 > y
113 > y
114 > y
115 > y
116 > EOF
117 > }
118
119 $ HGEDITOR=false runsplit
120 diff --git a/a b/a
121 1 hunks, 1 lines changed
122 examine changes to 'a'? [Ynesfdaq?] y
123
124 @@ -5,1 +5,1 @@ 4
125 -5
126 +55
127 record this change to 'a'? [Ynesfdaq?] y
128
129 transaction abort!
130 rollback completed
131 abort: edit failed: false exited with status 1
132 [255]
133 $ hg status
134
135 $ HGEDITOR="$PYTHON $TESTTMP/editor.py"
136 $ runsplit
137 diff --git a/a b/a
138 1 hunks, 1 lines changed
139 examine changes to 'a'? [Ynesfdaq?] y
140
141 @@ -5,1 +5,1 @@ 4
142 -5
143 +55
144 record this change to 'a'? [Ynesfdaq?] y
145
146 EDITOR: HG: Splitting 1df0d5c5a3ab. Write commit message for the first split changeset.
147 EDITOR: a2
148 EDITOR:
149 EDITOR:
150 EDITOR: HG: Enter commit message. Lines beginning with 'HG:' are removed.
151 EDITOR: HG: Leave message empty to abort commit.
152 EDITOR: HG: --
153 EDITOR: HG: user: test
154 EDITOR: HG: branch 'default'
155 EDITOR: HG: changed a
156 created new head
157 diff --git a/a b/a
158 1 hunks, 1 lines changed
159 examine changes to 'a'? [Ynesfdaq?] y
160
161 @@ -3,1 +3,1 @@ 2
162 -3
163 +33
164 record this change to 'a'? [Ynesfdaq?] y
165
166 EDITOR: HG: Splitting 1df0d5c5a3ab. So far it has been split into:
167 EDITOR: HG: - e704349bd21b: split 1
168 EDITOR: HG: Write commit message for the next split changeset.
169 EDITOR: a2
170 EDITOR:
171 EDITOR:
172 EDITOR: HG: Enter commit message. Lines beginning with 'HG:' are removed.
173 EDITOR: HG: Leave message empty to abort commit.
174 EDITOR: HG: --
175 EDITOR: HG: user: test
176 EDITOR: HG: branch 'default'
177 EDITOR: HG: changed a
178 diff --git a/a b/a
179 1 hunks, 1 lines changed
180 examine changes to 'a'? [Ynesfdaq?] y
181
182 @@ -1,1 +1,1 @@
183 -1
184 +11
185 record this change to 'a'? [Ynesfdaq?] y
186
187 EDITOR: HG: Splitting 1df0d5c5a3ab. So far it has been split into:
188 EDITOR: HG: - e704349bd21b: split 1
189 EDITOR: HG: - a09ad58faae3: split 2
190 EDITOR: HG: Write commit message for the next split changeset.
191 EDITOR: a2
192 EDITOR:
193 EDITOR:
194 EDITOR: HG: Enter commit message. Lines beginning with 'HG:' are removed.
195 EDITOR: HG: Leave message empty to abort commit.
196 EDITOR: HG: --
197 EDITOR: HG: user: test
198 EDITOR: HG: branch 'default'
199 EDITOR: HG: changed a
200 saved backup bundle to $TESTTMP/a/.hg/strip-backup/1df0d5c5a3ab-8341b760-split.hg (glob) (obsstore-off !)
201
202 #if obsstore-off
203 $ hg bookmark
204 r1 0:a61bcde8c529
205 r2 3:00eebaf8d2e2
206 * r3 3:00eebaf8d2e2
207 $ hg glog -p
208 @ 3:00eebaf8d2e2 split 3 r2 r3
209 | diff --git a/a b/a
210 | --- a/a
211 | +++ b/a
212 | @@ -1,1 +1,1 @@
213 | -1
214 | +11
215 |
216 o 2:a09ad58faae3 split 2
217 | diff --git a/a b/a
218 | --- a/a
219 | +++ b/a
220 | @@ -3,1 +3,1 @@
221 | -3
222 | +33
223 |
224 o 1:e704349bd21b split 1
225 | diff --git a/a b/a
226 | --- a/a
227 | +++ b/a
228 | @@ -5,1 +5,1 @@
229 | -5
230 | +55
231 |
232 o 0:a61bcde8c529 a1 r1
233 diff --git a/a b/a
234 new file mode 100644
235 --- /dev/null
236 +++ b/a
237 @@ -0,0 +1,5 @@
238 +1
239 +2
240 +3
241 +4
242 +5
243
244 #else
245 $ hg bookmark
246 r1 0:a61bcde8c529
247 r2 4:00eebaf8d2e2
248 * r3 4:00eebaf8d2e2
249 $ hg glog
250 @ 4:00eebaf8d2e2 split 3 r2 r3
251 |
252 o 3:a09ad58faae3 split 2
253 |
254 o 2:e704349bd21b split 1
255 |
256 o 0:a61bcde8c529 a1 r1
257
258 #endif
259
260 Split a head while working parent is not that head
261
262 $ cd $TESTTMP/b
263
264 $ hg up 0 -q
265 $ hg bookmark r3
266
267 $ runsplit tip >/dev/null
268
269 #if obsstore-off
270 $ hg bookmark
271 r1 0:a61bcde8c529
272 r2 3:00eebaf8d2e2
273 * r3 0:a61bcde8c529
274 $ hg glog
275 o 3:00eebaf8d2e2 split 3 r2
276 |
277 o 2:a09ad58faae3 split 2
278 |
279 o 1:e704349bd21b split 1
280 |
281 @ 0:a61bcde8c529 a1 r1 r3
282
283 #else
284 $ hg bookmark
285 r1 0:a61bcde8c529
286 r2 4:00eebaf8d2e2
287 * r3 0:a61bcde8c529
288 $ hg glog
289 o 4:00eebaf8d2e2 split 3 r2
290 |
291 o 3:a09ad58faae3 split 2
292 |
293 o 2:e704349bd21b split 1
294 |
295 @ 0:a61bcde8c529 a1 r1 r3
296
297 #endif
298
299 Split a non-head
300
301 $ cd $TESTTMP/c
302 $ echo d > d
303 $ hg ci -m d1 -A d
304 $ hg bookmark -i d1
305 $ echo 2 >> d
306 $ hg ci -m d2
307 $ echo 3 >> d
308 $ hg ci -m d3
309 $ hg bookmark -i d3
310 $ hg up '.^' -q
311 $ hg bookmark d2
312 $ cp -R . ../d
313
314 $ runsplit -r 1 | grep rebasing
315 rebasing 2:b5c5ea414030 "d1" (d1)
316 rebasing 3:f4a0a8d004cc "d2" (d2)
317 rebasing 4:777940761eba "d3" (d3)
318 #if obsstore-off
319 $ hg bookmark
320 d1 4:c4b449ef030e
321 * d2 5:c9dd00ab36a3
322 d3 6:19f476bc865c
323 r1 0:a61bcde8c529
324 r2 3:00eebaf8d2e2
325 $ hg glog -p
326 o 6:19f476bc865c d3 d3
327 | diff --git a/d b/d
328 | --- a/d
329 | +++ b/d
330 | @@ -2,0 +3,1 @@
331 | +3
332 |
333 @ 5:c9dd00ab36a3 d2 d2
334 | diff --git a/d b/d
335 | --- a/d
336 | +++ b/d
337 | @@ -1,0 +2,1 @@
338 | +2
339 |
340 o 4:c4b449ef030e d1 d1
341 | diff --git a/d b/d
342 | new file mode 100644
343 | --- /dev/null
344 | +++ b/d
345 | @@ -0,0 +1,1 @@
346 | +d
347 |
348 o 3:00eebaf8d2e2 split 3 r2
349 | diff --git a/a b/a
350 | --- a/a
351 | +++ b/a
352 | @@ -1,1 +1,1 @@
353 | -1
354 | +11
355 |
356 o 2:a09ad58faae3 split 2
357 | diff --git a/a b/a
358 | --- a/a
359 | +++ b/a
360 | @@ -3,1 +3,1 @@
361 | -3
362 | +33
363 |
364 o 1:e704349bd21b split 1
365 | diff --git a/a b/a
366 | --- a/a
367 | +++ b/a
368 | @@ -5,1 +5,1 @@
369 | -5
370 | +55
371 |
372 o 0:a61bcde8c529 a1 r1
373 diff --git a/a b/a
374 new file mode 100644
375 --- /dev/null
376 +++ b/a
377 @@ -0,0 +1,5 @@
378 +1
379 +2
380 +3
381 +4
382 +5
383
384 #else
385 $ hg bookmark
386 d1 8:c4b449ef030e
387 * d2 9:c9dd00ab36a3
388 d3 10:19f476bc865c
389 r1 0:a61bcde8c529
390 r2 7:00eebaf8d2e2
391 $ hg glog
392 o 10:19f476bc865c d3 d3
393 |
394 @ 9:c9dd00ab36a3 d2 d2
395 |
396 o 8:c4b449ef030e d1 d1
397 |
398 o 7:00eebaf8d2e2 split 3 r2
399 |
400 o 6:a09ad58faae3 split 2
401 |
402 o 5:e704349bd21b split 1
403 |
404 o 0:a61bcde8c529 a1 r1
405
406 #endif
407
408 Split a non-head without rebase
409
410 $ cd $TESTTMP/d
411 #if obsstore-off
412 $ runsplit -r 1 --no-rebase
413 abort: cannot split changeset with children without rebase
414 [255]
415 #else
416 $ runsplit -r 1 --no-rebase >/dev/null
417 $ hg bookmark
418 d1 2:b5c5ea414030
419 * d2 3:f4a0a8d004cc
420 d3 4:777940761eba
421 r1 0:a61bcde8c529
422 r2 7:00eebaf8d2e2
423
424 $ hg glog
425 o 7:00eebaf8d2e2 split 3 r2
426 |
427 o 6:a09ad58faae3 split 2
428 |
429 o 5:e704349bd21b split 1
430 |
431 | o 4:777940761eba d3 d3
432 | |
433 | @ 3:f4a0a8d004cc d2 d2
434 | |
435 | o 2:b5c5ea414030 d1 d1
436 | |
437 | x 1:1df0d5c5a3ab a2
438 |/
439 o 0:a61bcde8c529 a1 r1
440
441 #endif
442
443 Split a non-head with obsoleted descendants
444
445 #if obsstore-on
446 $ hg init $TESTTMP/e
447 $ cd $TESTTMP/e
448 $ hg debugdrawdag <<'EOS'
449 > H I J
450 > | | |
451 > F G1 G2 # amend: G1 -> G2
452 > | | / # prune: F
453 > C D E
454 > \|/
455 > B
456 > |
457 > A
458 > EOS
459 $ eval `hg tags -T '{tag}={node}\n'`
460 $ rm .hg/localtags
461 $ hg split $B --config experimental.evolution=createmarkers
462 abort: split would leave orphaned changesets behind
463 [255]
464 $ cat > $TESTTMP/messages <<EOF
465 > Split B
466 > EOF
467 $ cat <<EOF | hg split $B
468 > y
469 > y
470 > EOF
471 diff --git a/B b/B
472 new file mode 100644
473 examine changes to 'B'? [Ynesfdaq?] y
474
475 @@ -0,0 +1,1 @@
476 +B
477 \ No newline at end of file
478 record this change to 'B'? [Ynesfdaq?] y
479
480 EDITOR: HG: Splitting 112478962961. Write commit message for the first split changeset.
481 EDITOR: B
482 EDITOR:
483 EDITOR:
484 EDITOR: HG: Enter commit message. Lines beginning with 'HG:' are removed.
485 EDITOR: HG: Leave message empty to abort commit.
486 EDITOR: HG: --
487 EDITOR: HG: user: test
488 EDITOR: HG: branch 'default'
489 EDITOR: HG: added B
490 created new head
491 rebasing 2:26805aba1e60 "C"
492 rebasing 3:be0ef73c17ad "D"
493 rebasing 4:49cb92066bfd "E"
494 rebasing 7:97a6268cc7ef "G2"
495 rebasing 10:e2f1e425c0db "J"
496 $ hg glog -r 'sort(all(), topo)'
497 o 16:556c085f8b52 J
498 |
499 o 15:8761f6c9123f G2
500 |
501 o 14:a7aeffe59b65 E
502 |
503 | o 13:e1e914ede9ab D
504 |/
505 | o 12:01947e9b98aa C
506 |/
507 o 11:0947baa74d47 Split B
508 |
509 | o 9:88ede1d5ee13 I
510 | |
511 | x 6:af8cbf225b7b G1
512 | |
513 | x 3:be0ef73c17ad D
514 | |
515 | | o 8:74863e5b5074 H
516 | | |
517 | | x 5:ee481a2a1e69 F
518 | | |
519 | | x 2:26805aba1e60 C
520 | |/
521 | x 1:112478962961 B
522 |/
523 o 0:426bada5c675 A
524
525 #endif
General Comments 0
You need to be logged in to leave comments. Login now