##// END OF EJS Templates
rebase: use "rebase.collapse" as "editform" for "--collapse" always...
FUJIWARA Katsunori -
r22206:6122ad50 default
parent child Browse files
Show More
@@ -1,974 +1,976 b''
1 1 # rebase.py - rebasing feature for mercurial
2 2 #
3 3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to move sets of revisions to a different ancestor
9 9
10 10 This extension lets you rebase changesets in an existing Mercurial
11 11 repository.
12 12
13 13 For more information:
14 14 http://mercurial.selenic.com/wiki/RebaseExtension
15 15 '''
16 16
17 17 from mercurial import hg, util, repair, merge, cmdutil, commands, bookmarks
18 18 from mercurial import extensions, patch, scmutil, phases, obsolete, error
19 19 from mercurial.commands import templateopts
20 20 from mercurial.node import nullrev
21 21 from mercurial.lock import release
22 22 from mercurial.i18n import _
23 23 import os, errno
24 24
25 25 nullmerge = -2
26 26 revignored = -3
27 27
28 28 cmdtable = {}
29 29 command = cmdutil.command(cmdtable)
30 30 testedwith = 'internal'
31 31
32 32 def _savegraft(ctx, extra):
33 33 s = ctx.extra().get('source', None)
34 34 if s is not None:
35 35 extra['source'] = s
36 36
37 37 def _savebranch(ctx, extra):
38 38 extra['branch'] = ctx.branch()
39 39
40 40 def _makeextrafn(copiers):
41 41 """make an extrafn out of the given copy-functions.
42 42
43 43 A copy function takes a context and an extra dict, and mutates the
44 44 extra dict as needed based on the given context.
45 45 """
46 46 def extrafn(ctx, extra):
47 47 for c in copiers:
48 48 c(ctx, extra)
49 49 return extrafn
50 50
51 51 @command('rebase',
52 52 [('s', 'source', '',
53 53 _('rebase from the specified changeset'), _('REV')),
54 54 ('b', 'base', '',
55 55 _('rebase from the base of the specified changeset '
56 56 '(up to greatest common ancestor of base and dest)'),
57 57 _('REV')),
58 58 ('r', 'rev', [],
59 59 _('rebase these revisions'),
60 60 _('REV')),
61 61 ('d', 'dest', '',
62 62 _('rebase onto the specified changeset'), _('REV')),
63 63 ('', 'collapse', False, _('collapse the rebased changesets')),
64 64 ('m', 'message', '',
65 65 _('use text as collapse commit message'), _('TEXT')),
66 66 ('e', 'edit', False, _('invoke editor on commit messages')),
67 67 ('l', 'logfile', '',
68 68 _('read collapse commit message from file'), _('FILE')),
69 69 ('', 'keep', False, _('keep original changesets')),
70 70 ('', 'keepbranches', False, _('keep original branch names')),
71 71 ('D', 'detach', False, _('(DEPRECATED)')),
72 72 ('t', 'tool', '', _('specify merge tool')),
73 73 ('c', 'continue', False, _('continue an interrupted rebase')),
74 74 ('a', 'abort', False, _('abort an interrupted rebase'))] +
75 75 templateopts,
76 76 _('[-s REV | -b REV] [-d REV] [OPTION]'))
77 77 def rebase(ui, repo, **opts):
78 78 """move changeset (and descendants) to a different branch
79 79
80 80 Rebase uses repeated merging to graft changesets from one part of
81 81 history (the source) onto another (the destination). This can be
82 82 useful for linearizing *local* changes relative to a master
83 83 development tree.
84 84
85 85 You should not rebase changesets that have already been shared
86 86 with others. Doing so will force everybody else to perform the
87 87 same rebase or they will end up with duplicated changesets after
88 88 pulling in your rebased changesets.
89 89
90 90 In its default configuration, Mercurial will prevent you from
91 91 rebasing published changes. See :hg:`help phases` for details.
92 92
93 93 If you don't specify a destination changeset (``-d/--dest``),
94 94 rebase uses the current branch tip as the destination. (The
95 95 destination changeset is not modified by rebasing, but new
96 96 changesets are added as its descendants.)
97 97
98 98 You can specify which changesets to rebase in two ways: as a
99 99 "source" changeset or as a "base" changeset. Both are shorthand
100 100 for a topologically related set of changesets (the "source
101 101 branch"). If you specify source (``-s/--source``), rebase will
102 102 rebase that changeset and all of its descendants onto dest. If you
103 103 specify base (``-b/--base``), rebase will select ancestors of base
104 104 back to but not including the common ancestor with dest. Thus,
105 105 ``-b`` is less precise but more convenient than ``-s``: you can
106 106 specify any changeset in the source branch, and rebase will select
107 107 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
108 108 uses the parent of the working directory as the base.
109 109
110 110 For advanced usage, a third way is available through the ``--rev``
111 111 option. It allows you to specify an arbitrary set of changesets to
112 112 rebase. Descendants of revs you specify with this option are not
113 113 automatically included in the rebase.
114 114
115 115 By default, rebase recreates the changesets in the source branch
116 116 as descendants of dest and then destroys the originals. Use
117 117 ``--keep`` to preserve the original source changesets. Some
118 118 changesets in the source branch (e.g. merges from the destination
119 119 branch) may be dropped if they no longer contribute any change.
120 120
121 121 One result of the rules for selecting the destination changeset
122 122 and source branch is that, unlike ``merge``, rebase will do
123 123 nothing if you are at the branch tip of a named branch
124 124 with two heads. You need to explicitly specify source and/or
125 125 destination (or ``update`` to the other head, if it's the head of
126 126 the intended source branch).
127 127
128 128 If a rebase is interrupted to manually resolve a merge, it can be
129 129 continued with --continue/-c or aborted with --abort/-a.
130 130
131 131 Returns 0 on success, 1 if nothing to rebase or there are
132 132 unresolved conflicts.
133 133 """
134 134 originalwd = target = None
135 135 activebookmark = None
136 136 external = nullrev
137 137 state = {}
138 138 skipped = set()
139 139 targetancestors = set()
140 140
141 141 editor = cmdutil.getcommiteditor(editform='rebase.normal', **opts)
142 142
143 143 lock = wlock = None
144 144 try:
145 145 wlock = repo.wlock()
146 146 lock = repo.lock()
147 147
148 148 # Validate input and define rebasing points
149 149 destf = opts.get('dest', None)
150 150 srcf = opts.get('source', None)
151 151 basef = opts.get('base', None)
152 152 revf = opts.get('rev', [])
153 153 contf = opts.get('continue')
154 154 abortf = opts.get('abort')
155 155 collapsef = opts.get('collapse', False)
156 156 collapsemsg = cmdutil.logmessage(ui, opts)
157 157 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
158 158 extrafns = [_savegraft]
159 159 if e:
160 160 extrafns = [e]
161 161 keepf = opts.get('keep', False)
162 162 keepbranchesf = opts.get('keepbranches', False)
163 163 # keepopen is not meant for use on the command line, but by
164 164 # other extensions
165 165 keepopen = opts.get('keepopen', False)
166 166
167 167 if collapsemsg and not collapsef:
168 168 raise util.Abort(
169 169 _('message can only be specified with collapse'))
170 170
171 171 if contf or abortf:
172 172 if contf and abortf:
173 173 raise util.Abort(_('cannot use both abort and continue'))
174 174 if collapsef:
175 175 raise util.Abort(
176 176 _('cannot use collapse with continue or abort'))
177 177 if srcf or basef or destf:
178 178 raise util.Abort(
179 179 _('abort and continue do not allow specifying revisions'))
180 180 if opts.get('tool', False):
181 181 ui.warn(_('tool option will be ignored\n'))
182 182
183 183 try:
184 184 (originalwd, target, state, skipped, collapsef, keepf,
185 185 keepbranchesf, external, activebookmark) = restorestatus(repo)
186 186 except error.RepoLookupError:
187 187 if abortf:
188 188 clearstatus(repo)
189 189 repo.ui.warn(_('rebase aborted (no revision is removed,'
190 190 ' only broken state is cleared)\n'))
191 191 return 0
192 192 else:
193 193 msg = _('cannot continue inconsistent rebase')
194 194 hint = _('use "hg rebase --abort" to clear broken state')
195 195 raise util.Abort(msg, hint=hint)
196 196 if abortf:
197 197 return abort(repo, originalwd, target, state)
198 198 else:
199 199 if srcf and basef:
200 200 raise util.Abort(_('cannot specify both a '
201 201 'source and a base'))
202 202 if revf and basef:
203 203 raise util.Abort(_('cannot specify both a '
204 204 'revision and a base'))
205 205 if revf and srcf:
206 206 raise util.Abort(_('cannot specify both a '
207 207 'revision and a source'))
208 208
209 209 cmdutil.checkunfinished(repo)
210 210 cmdutil.bailifchanged(repo)
211 211
212 212 if not destf:
213 213 # Destination defaults to the latest revision in the
214 214 # current branch
215 215 branch = repo[None].branch()
216 216 dest = repo[branch]
217 217 else:
218 218 dest = scmutil.revsingle(repo, destf)
219 219
220 220 if revf:
221 221 rebaseset = scmutil.revrange(repo, revf)
222 222 if not rebaseset:
223 223 ui.status(_('empty "rev" revision set - '
224 224 'nothing to rebase\n'))
225 225 return 1
226 226 elif srcf:
227 227 src = scmutil.revrange(repo, [srcf])
228 228 if not src:
229 229 ui.status(_('empty "source" revision set - '
230 230 'nothing to rebase\n'))
231 231 return 1
232 232 rebaseset = repo.revs('(%ld)::', src)
233 233 assert rebaseset
234 234 else:
235 235 base = scmutil.revrange(repo, [basef or '.'])
236 236 if not base:
237 237 ui.status(_('empty "base" revision set - '
238 238 "can't compute rebase set\n"))
239 239 return 1
240 240 rebaseset = repo.revs(
241 241 '(children(ancestor(%ld, %d)) and ::(%ld))::',
242 242 base, dest, base)
243 243 if not rebaseset:
244 244 if base == [dest.rev()]:
245 245 if basef:
246 246 ui.status(_('nothing to rebase - %s is both "base"'
247 247 ' and destination\n') % dest)
248 248 else:
249 249 ui.status(_('nothing to rebase - working directory '
250 250 'parent is also destination\n'))
251 251 elif not repo.revs('%ld - ::%d', base, dest):
252 252 if basef:
253 253 ui.status(_('nothing to rebase - "base" %s is '
254 254 'already an ancestor of destination '
255 255 '%s\n') %
256 256 ('+'.join(str(repo[r]) for r in base),
257 257 dest))
258 258 else:
259 259 ui.status(_('nothing to rebase - working '
260 260 'directory parent is already an '
261 261 'ancestor of destination %s\n') % dest)
262 262 else: # can it happen?
263 263 ui.status(_('nothing to rebase from %s to %s\n') %
264 264 ('+'.join(str(repo[r]) for r in base), dest))
265 265 return 1
266 266
267 267 if (not (keepf or obsolete._enabled)
268 268 and repo.revs('first(children(%ld) - %ld)',
269 269 rebaseset, rebaseset)):
270 270 raise util.Abort(
271 271 _("can't remove original changesets with"
272 272 " unrebased descendants"),
273 273 hint=_('use --keep to keep original changesets'))
274 274
275 275 result = buildstate(repo, dest, rebaseset, collapsef)
276 276 if not result:
277 277 # Empty state built, nothing to rebase
278 278 ui.status(_('nothing to rebase\n'))
279 279 return 1
280 280
281 281 root = min(rebaseset)
282 282 if not keepf and not repo[root].mutable():
283 283 raise util.Abort(_("can't rebase immutable changeset %s")
284 284 % repo[root],
285 285 hint=_('see hg help phases for details'))
286 286
287 287 originalwd, target, state = result
288 288 if collapsef:
289 289 targetancestors = repo.changelog.ancestors([target],
290 290 inclusive=True)
291 291 external = externalparent(repo, state, targetancestors)
292 292
293 293 if dest.closesbranch() and not keepbranchesf:
294 294 ui.status(_('reopening closed branch head %s\n') % dest)
295 295
296 296 if keepbranchesf:
297 297 # insert _savebranch at the start of extrafns so if
298 298 # there's a user-provided extrafn it can clobber branch if
299 299 # desired
300 300 extrafns.insert(0, _savebranch)
301 301 if collapsef:
302 302 branches = set()
303 303 for rev in state:
304 304 branches.add(repo[rev].branch())
305 305 if len(branches) > 1:
306 306 raise util.Abort(_('cannot collapse multiple named '
307 307 'branches'))
308 308
309 309 # Rebase
310 310 if not targetancestors:
311 311 targetancestors = repo.changelog.ancestors([target], inclusive=True)
312 312
313 313 # Keep track of the current bookmarks in order to reset them later
314 314 currentbookmarks = repo._bookmarks.copy()
315 315 activebookmark = activebookmark or repo._bookmarkcurrent
316 316 if activebookmark:
317 317 bookmarks.unsetcurrent(repo)
318 318
319 319 extrafn = _makeextrafn(extrafns)
320 320
321 321 sortedstate = sorted(state)
322 322 total = len(sortedstate)
323 323 pos = 0
324 324 for rev in sortedstate:
325 325 pos += 1
326 326 if state[rev] == -1:
327 327 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
328 328 _('changesets'), total)
329 329 p1, p2 = defineparents(repo, rev, target, state,
330 330 targetancestors)
331 331 storestatus(repo, originalwd, target, state, collapsef, keepf,
332 332 keepbranchesf, external, activebookmark)
333 333 if len(repo.parents()) == 2:
334 334 repo.ui.debug('resuming interrupted rebase\n')
335 335 else:
336 336 try:
337 337 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
338 338 'rebase')
339 339 stats = rebasenode(repo, rev, p1, state, collapsef)
340 340 if stats and stats[3] > 0:
341 341 raise error.InterventionRequired(
342 342 _('unresolved conflicts (see hg '
343 343 'resolve, then hg rebase --continue)'))
344 344 finally:
345 345 ui.setconfig('ui', 'forcemerge', '', 'rebase')
346 346 if collapsef:
347 347 cmdutil.duplicatecopies(repo, rev, target)
348 348 else:
349 349 # If we're not using --collapse, we need to
350 350 # duplicate copies between the revision we're
351 351 # rebasing and its first parent, but *not*
352 352 # duplicate any copies that have already been
353 353 # performed in the destination.
354 354 p1rev = repo[rev].p1().rev()
355 355 cmdutil.duplicatecopies(repo, rev, p1rev, skiprev=target)
356 356 if not collapsef:
357 357 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
358 358 editor=editor)
359 359 else:
360 360 # Skip commit if we are collapsing
361 361 repo.setparents(repo[p1].node())
362 362 newrev = None
363 363 # Update the state
364 364 if newrev is not None:
365 365 state[rev] = repo[newrev].rev()
366 366 else:
367 367 if not collapsef:
368 368 ui.note(_('no changes, revision %d skipped\n') % rev)
369 369 ui.debug('next revision set to %s\n' % p1)
370 370 skipped.add(rev)
371 371 state[rev] = p1
372 372
373 373 ui.progress(_('rebasing'), None)
374 374 ui.note(_('rebase merging completed\n'))
375 375
376 376 if collapsef and not keepopen:
377 377 p1, p2 = defineparents(repo, min(state), target,
378 378 state, targetancestors)
379 editopt = opts.get('edit')
380 editform = 'rebase.collapse'
379 381 if collapsemsg:
380 382 commitmsg = collapsemsg
381 383 else:
382 384 commitmsg = 'Collapsed revision'
383 385 for rebased in state:
384 386 if rebased not in skipped and state[rebased] > nullmerge:
385 387 commitmsg += '\n* %s' % repo[rebased].description()
386 editform = 'rebase.collapse'
387 editor = cmdutil.getcommiteditor(edit=True, editform=editform)
388 editopt = True
389 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
388 390 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
389 391 extrafn=extrafn, editor=editor)
390 392 for oldrev in state.iterkeys():
391 393 if state[oldrev] > nullmerge:
392 394 state[oldrev] = newrev
393 395
394 396 if 'qtip' in repo.tags():
395 397 updatemq(repo, state, skipped, **opts)
396 398
397 399 if currentbookmarks:
398 400 # Nodeids are needed to reset bookmarks
399 401 nstate = {}
400 402 for k, v in state.iteritems():
401 403 if v > nullmerge:
402 404 nstate[repo[k].node()] = repo[v].node()
403 405 # XXX this is the same as dest.node() for the non-continue path --
404 406 # this should probably be cleaned up
405 407 targetnode = repo[target].node()
406 408
407 409 # restore original working directory
408 410 # (we do this before stripping)
409 411 newwd = state.get(originalwd, originalwd)
410 412 if newwd not in [c.rev() for c in repo[None].parents()]:
411 413 ui.note(_("update back to initial working directory parent\n"))
412 414 hg.updaterepo(repo, newwd, False)
413 415
414 416 if not keepf:
415 417 collapsedas = None
416 418 if collapsef:
417 419 collapsedas = newrev
418 420 clearrebased(ui, repo, state, skipped, collapsedas)
419 421
420 422 if currentbookmarks:
421 423 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
422 424 if activebookmark not in repo._bookmarks:
423 425 # active bookmark was divergent one and has been deleted
424 426 activebookmark = None
425 427
426 428 clearstatus(repo)
427 429 ui.note(_("rebase completed\n"))
428 430 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
429 431 if skipped:
430 432 ui.note(_("%d revisions have been skipped\n") % len(skipped))
431 433
432 434 if (activebookmark and
433 435 repo['.'].node() == repo._bookmarks[activebookmark]):
434 436 bookmarks.setcurrent(repo, activebookmark)
435 437
436 438 finally:
437 439 release(lock, wlock)
438 440
439 441 def externalparent(repo, state, targetancestors):
440 442 """Return the revision that should be used as the second parent
441 443 when the revisions in state is collapsed on top of targetancestors.
442 444 Abort if there is more than one parent.
443 445 """
444 446 parents = set()
445 447 source = min(state)
446 448 for rev in state:
447 449 if rev == source:
448 450 continue
449 451 for p in repo[rev].parents():
450 452 if (p.rev() not in state
451 453 and p.rev() not in targetancestors):
452 454 parents.add(p.rev())
453 455 if not parents:
454 456 return nullrev
455 457 if len(parents) == 1:
456 458 return parents.pop()
457 459 raise util.Abort(_('unable to collapse on top of %s, there is more '
458 460 'than one external parent: %s') %
459 461 (max(targetancestors),
460 462 ', '.join(str(p) for p in sorted(parents))))
461 463
462 464 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
463 465 'Commit the changes and store useful information in extra'
464 466 try:
465 467 repo.setparents(repo[p1].node(), repo[p2].node())
466 468 ctx = repo[rev]
467 469 if commitmsg is None:
468 470 commitmsg = ctx.description()
469 471 extra = {'rebase_source': ctx.hex()}
470 472 if extrafn:
471 473 extrafn(ctx, extra)
472 474
473 475 backup = repo.ui.backupconfig('phases', 'new-commit')
474 476 try:
475 477 targetphase = max(ctx.phase(), phases.draft)
476 478 repo.ui.setconfig('phases', 'new-commit', targetphase, 'rebase')
477 479 # Commit might fail if unresolved files exist
478 480 newrev = repo.commit(text=commitmsg, user=ctx.user(),
479 481 date=ctx.date(), extra=extra, editor=editor)
480 482 finally:
481 483 repo.ui.restoreconfig(backup)
482 484
483 485 repo.dirstate.setbranch(repo[newrev].branch())
484 486 return newrev
485 487 except util.Abort:
486 488 # Invalidate the previous setparents
487 489 repo.dirstate.invalidate()
488 490 raise
489 491
490 492 def rebasenode(repo, rev, p1, state, collapse):
491 493 'Rebase a single revision'
492 494 # Merge phase
493 495 # Update to target and merge it with local
494 496 if repo['.'].rev() != repo[p1].rev():
495 497 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
496 498 merge.update(repo, p1, False, True, False)
497 499 else:
498 500 repo.ui.debug(" already in target\n")
499 501 repo.dirstate.write()
500 502 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
501 503 if repo[rev].rev() == repo[min(state)].rev():
502 504 # Case (1) initial changeset of a non-detaching rebase.
503 505 # Let the merge mechanism find the base itself.
504 506 base = None
505 507 elif not repo[rev].p2():
506 508 # Case (2) detaching the node with a single parent, use this parent
507 509 base = repo[rev].p1().node()
508 510 else:
509 511 # In case of merge, we need to pick the right parent as merge base.
510 512 #
511 513 # Imagine we have:
512 514 # - M: currently rebase revision in this step
513 515 # - A: one parent of M
514 516 # - B: second parent of M
515 517 # - D: destination of this merge step (p1 var)
516 518 #
517 519 # If we are rebasing on D, D is the successors of A or B. The right
518 520 # merge base is the one D succeed to. We pretend it is B for the rest
519 521 # of this comment
520 522 #
521 523 # If we pick B as the base, the merge involves:
522 524 # - changes from B to M (actual changeset payload)
523 525 # - changes from B to D (induced by rebase) as D is a rebased
524 526 # version of B)
525 527 # Which exactly represent the rebase operation.
526 528 #
527 529 # If we pick the A as the base, the merge involves
528 530 # - changes from A to M (actual changeset payload)
529 531 # - changes from A to D (with include changes between unrelated A and B
530 532 # plus changes induced by rebase)
531 533 # Which does not represent anything sensible and creates a lot of
532 534 # conflicts.
533 535 for p in repo[rev].parents():
534 536 if state.get(p.rev()) == repo[p1].rev():
535 537 base = p.node()
536 538 break
537 539 else: # fallback when base not found
538 540 base = None
539 541
540 542 # Raise because this function is called wrong (see issue 4106)
541 543 raise AssertionError('no base found to rebase on '
542 544 '(rebasenode called wrong)')
543 545 if base is not None:
544 546 repo.ui.debug(" detach base %d:%s\n" % (repo[base].rev(), repo[base]))
545 547 # When collapsing in-place, the parent is the common ancestor, we
546 548 # have to allow merging with it.
547 549 return merge.update(repo, rev, True, True, False, base, collapse,
548 550 labels=['dest', 'source'])
549 551
550 552 def nearestrebased(repo, rev, state):
551 553 """return the nearest ancestors of rev in the rebase result"""
552 554 rebased = [r for r in state if state[r] > nullmerge]
553 555 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
554 556 if candidates:
555 557 return state[candidates[0]]
556 558 else:
557 559 return None
558 560
559 561 def defineparents(repo, rev, target, state, targetancestors):
560 562 'Return the new parent relationship of the revision that will be rebased'
561 563 parents = repo[rev].parents()
562 564 p1 = p2 = nullrev
563 565
564 566 P1n = parents[0].rev()
565 567 if P1n in targetancestors:
566 568 p1 = target
567 569 elif P1n in state:
568 570 if state[P1n] == nullmerge:
569 571 p1 = target
570 572 elif state[P1n] == revignored:
571 573 p1 = nearestrebased(repo, P1n, state)
572 574 if p1 is None:
573 575 p1 = target
574 576 else:
575 577 p1 = state[P1n]
576 578 else: # P1n external
577 579 p1 = target
578 580 p2 = P1n
579 581
580 582 if len(parents) == 2 and parents[1].rev() not in targetancestors:
581 583 P2n = parents[1].rev()
582 584 # interesting second parent
583 585 if P2n in state:
584 586 if p1 == target: # P1n in targetancestors or external
585 587 p1 = state[P2n]
586 588 elif state[P2n] == revignored:
587 589 p2 = nearestrebased(repo, P2n, state)
588 590 if p2 is None:
589 591 # no ancestors rebased yet, detach
590 592 p2 = target
591 593 else:
592 594 p2 = state[P2n]
593 595 else: # P2n external
594 596 if p2 != nullrev: # P1n external too => rev is a merged revision
595 597 raise util.Abort(_('cannot use revision %d as base, result '
596 598 'would have 3 parents') % rev)
597 599 p2 = P2n
598 600 repo.ui.debug(" future parents are %d and %d\n" %
599 601 (repo[p1].rev(), repo[p2].rev()))
600 602 return p1, p2
601 603
602 604 def isagitpatch(repo, patchname):
603 605 'Return true if the given patch is in git format'
604 606 mqpatch = os.path.join(repo.mq.path, patchname)
605 607 for line in patch.linereader(file(mqpatch, 'rb')):
606 608 if line.startswith('diff --git'):
607 609 return True
608 610 return False
609 611
610 612 def updatemq(repo, state, skipped, **opts):
611 613 'Update rebased mq patches - finalize and then import them'
612 614 mqrebase = {}
613 615 mq = repo.mq
614 616 original_series = mq.fullseries[:]
615 617 skippedpatches = set()
616 618
617 619 for p in mq.applied:
618 620 rev = repo[p.node].rev()
619 621 if rev in state:
620 622 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
621 623 (rev, p.name))
622 624 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
623 625 else:
624 626 # Applied but not rebased, not sure this should happen
625 627 skippedpatches.add(p.name)
626 628
627 629 if mqrebase:
628 630 mq.finish(repo, mqrebase.keys())
629 631
630 632 # We must start import from the newest revision
631 633 for rev in sorted(mqrebase, reverse=True):
632 634 if rev not in skipped:
633 635 name, isgit = mqrebase[rev]
634 636 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
635 637 mq.qimport(repo, (), patchname=name, git=isgit,
636 638 rev=[str(state[rev])])
637 639 else:
638 640 # Rebased and skipped
639 641 skippedpatches.add(mqrebase[rev][0])
640 642
641 643 # Patches were either applied and rebased and imported in
642 644 # order, applied and removed or unapplied. Discard the removed
643 645 # ones while preserving the original series order and guards.
644 646 newseries = [s for s in original_series
645 647 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
646 648 mq.fullseries[:] = newseries
647 649 mq.seriesdirty = True
648 650 mq.savedirty()
649 651
650 652 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
651 653 'Move bookmarks to their correct changesets, and delete divergent ones'
652 654 marks = repo._bookmarks
653 655 for k, v in originalbookmarks.iteritems():
654 656 if v in nstate:
655 657 # update the bookmarks for revs that have moved
656 658 marks[k] = nstate[v]
657 659 bookmarks.deletedivergent(repo, [targetnode], k)
658 660
659 661 marks.write()
660 662
661 663 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
662 664 external, activebookmark):
663 665 'Store the current status to allow recovery'
664 666 f = repo.opener("rebasestate", "w")
665 667 f.write(repo[originalwd].hex() + '\n')
666 668 f.write(repo[target].hex() + '\n')
667 669 f.write(repo[external].hex() + '\n')
668 670 f.write('%d\n' % int(collapse))
669 671 f.write('%d\n' % int(keep))
670 672 f.write('%d\n' % int(keepbranches))
671 673 f.write('%s\n' % (activebookmark or ''))
672 674 for d, v in state.iteritems():
673 675 oldrev = repo[d].hex()
674 676 if v > nullmerge:
675 677 newrev = repo[v].hex()
676 678 else:
677 679 newrev = v
678 680 f.write("%s:%s\n" % (oldrev, newrev))
679 681 f.close()
680 682 repo.ui.debug('rebase status stored\n')
681 683
682 684 def clearstatus(repo):
683 685 'Remove the status files'
684 686 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
685 687
686 688 def restorestatus(repo):
687 689 'Restore a previously stored status'
688 690 try:
689 691 keepbranches = None
690 692 target = None
691 693 collapse = False
692 694 external = nullrev
693 695 activebookmark = None
694 696 state = {}
695 697 f = repo.opener("rebasestate")
696 698 for i, l in enumerate(f.read().splitlines()):
697 699 if i == 0:
698 700 originalwd = repo[l].rev()
699 701 elif i == 1:
700 702 target = repo[l].rev()
701 703 elif i == 2:
702 704 external = repo[l].rev()
703 705 elif i == 3:
704 706 collapse = bool(int(l))
705 707 elif i == 4:
706 708 keep = bool(int(l))
707 709 elif i == 5:
708 710 keepbranches = bool(int(l))
709 711 elif i == 6 and not (len(l) == 81 and ':' in l):
710 712 # line 6 is a recent addition, so for backwards compatibility
711 713 # check that the line doesn't look like the oldrev:newrev lines
712 714 activebookmark = l
713 715 else:
714 716 oldrev, newrev = l.split(':')
715 717 if newrev in (str(nullmerge), str(revignored)):
716 718 state[repo[oldrev].rev()] = int(newrev)
717 719 else:
718 720 state[repo[oldrev].rev()] = repo[newrev].rev()
719 721
720 722 if keepbranches is None:
721 723 raise util.Abort(_('.hg/rebasestate is incomplete'))
722 724
723 725 skipped = set()
724 726 # recompute the set of skipped revs
725 727 if not collapse:
726 728 seen = set([target])
727 729 for old, new in sorted(state.items()):
728 730 if new != nullrev and new in seen:
729 731 skipped.add(old)
730 732 seen.add(new)
731 733 repo.ui.debug('computed skipped revs: %s\n' %
732 734 (' '.join(str(r) for r in sorted(skipped)) or None))
733 735 repo.ui.debug('rebase status resumed\n')
734 736 return (originalwd, target, state, skipped,
735 737 collapse, keep, keepbranches, external, activebookmark)
736 738 except IOError, err:
737 739 if err.errno != errno.ENOENT:
738 740 raise
739 741 raise util.Abort(_('no rebase in progress'))
740 742
741 743 def inrebase(repo, originalwd, state):
742 744 '''check whether the working dir is in an interrupted rebase'''
743 745 parents = [p.rev() for p in repo.parents()]
744 746 if originalwd in parents:
745 747 return True
746 748
747 749 for newrev in state.itervalues():
748 750 if newrev in parents:
749 751 return True
750 752
751 753 return False
752 754
753 755 def abort(repo, originalwd, target, state):
754 756 'Restore the repository to its original state'
755 757 dstates = [s for s in state.values() if s > nullrev]
756 758 immutable = [d for d in dstates if not repo[d].mutable()]
757 759 cleanup = True
758 760 if immutable:
759 761 repo.ui.warn(_("warning: can't clean up immutable changesets %s\n")
760 762 % ', '.join(str(repo[r]) for r in immutable),
761 763 hint=_('see hg help phases for details'))
762 764 cleanup = False
763 765
764 766 descendants = set()
765 767 if dstates:
766 768 descendants = set(repo.changelog.descendants(dstates))
767 769 if descendants - set(dstates):
768 770 repo.ui.warn(_("warning: new changesets detected on target branch, "
769 771 "can't strip\n"))
770 772 cleanup = False
771 773
772 774 if cleanup:
773 775 # Update away from the rebase if necessary
774 776 if inrebase(repo, originalwd, state):
775 777 merge.update(repo, repo[originalwd].rev(), False, True, False)
776 778
777 779 # Strip from the first rebased revision
778 780 rebased = filter(lambda x: x > -1 and x != target, state.values())
779 781 if rebased:
780 782 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
781 783 # no backup of rebased cset versions needed
782 784 repair.strip(repo.ui, repo, strippoints)
783 785
784 786 clearstatus(repo)
785 787 repo.ui.warn(_('rebase aborted\n'))
786 788 return 0
787 789
788 790 def buildstate(repo, dest, rebaseset, collapse):
789 791 '''Define which revisions are going to be rebased and where
790 792
791 793 repo: repo
792 794 dest: context
793 795 rebaseset: set of rev
794 796 '''
795 797
796 798 # This check isn't strictly necessary, since mq detects commits over an
797 799 # applied patch. But it prevents messing up the working directory when
798 800 # a partially completed rebase is blocked by mq.
799 801 if 'qtip' in repo.tags() and (dest.node() in
800 802 [s.node for s in repo.mq.applied]):
801 803 raise util.Abort(_('cannot rebase onto an applied mq patch'))
802 804
803 805 roots = list(repo.set('roots(%ld)', rebaseset))
804 806 if not roots:
805 807 raise util.Abort(_('no matching revisions'))
806 808 roots.sort()
807 809 state = {}
808 810 detachset = set()
809 811 for root in roots:
810 812 commonbase = root.ancestor(dest)
811 813 if commonbase == root:
812 814 raise util.Abort(_('source is ancestor of destination'))
813 815 if commonbase == dest:
814 816 samebranch = root.branch() == dest.branch()
815 817 if not collapse and samebranch and root in dest.children():
816 818 repo.ui.debug('source is a child of destination\n')
817 819 return None
818 820
819 821 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, root))
820 822 state.update(dict.fromkeys(rebaseset, nullrev))
821 823 # Rebase tries to turn <dest> into a parent of <root> while
822 824 # preserving the number of parents of rebased changesets:
823 825 #
824 826 # - A changeset with a single parent will always be rebased as a
825 827 # changeset with a single parent.
826 828 #
827 829 # - A merge will be rebased as merge unless its parents are both
828 830 # ancestors of <dest> or are themselves in the rebased set and
829 831 # pruned while rebased.
830 832 #
831 833 # If one parent of <root> is an ancestor of <dest>, the rebased
832 834 # version of this parent will be <dest>. This is always true with
833 835 # --base option.
834 836 #
835 837 # Otherwise, we need to *replace* the original parents with
836 838 # <dest>. This "detaches" the rebased set from its former location
837 839 # and rebases it onto <dest>. Changes introduced by ancestors of
838 840 # <root> not common with <dest> (the detachset, marked as
839 841 # nullmerge) are "removed" from the rebased changesets.
840 842 #
841 843 # - If <root> has a single parent, set it to <dest>.
842 844 #
843 845 # - If <root> is a merge, we cannot decide which parent to
844 846 # replace, the rebase operation is not clearly defined.
845 847 #
846 848 # The table below sums up this behavior:
847 849 #
848 850 # +------------------+----------------------+-------------------------+
849 851 # | | one parent | merge |
850 852 # +------------------+----------------------+-------------------------+
851 853 # | parent in | new parent is <dest> | parents in ::<dest> are |
852 854 # | ::<dest> | | remapped to <dest> |
853 855 # +------------------+----------------------+-------------------------+
854 856 # | unrelated source | new parent is <dest> | ambiguous, abort |
855 857 # +------------------+----------------------+-------------------------+
856 858 #
857 859 # The actual abort is handled by `defineparents`
858 860 if len(root.parents()) <= 1:
859 861 # ancestors of <root> not ancestors of <dest>
860 862 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
861 863 [root.rev()]))
862 864 for r in detachset:
863 865 if r not in state:
864 866 state[r] = nullmerge
865 867 if len(roots) > 1:
866 868 # If we have multiple roots, we may have "hole" in the rebase set.
867 869 # Rebase roots that descend from those "hole" should not be detached as
868 870 # other root are. We use the special `revignored` to inform rebase that
869 871 # the revision should be ignored but that `defineparents` should search
870 872 # a rebase destination that make sense regarding rebased topology.
871 873 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
872 874 for ignored in set(rebasedomain) - set(rebaseset):
873 875 state[ignored] = revignored
874 876 return repo['.'].rev(), dest.rev(), state
875 877
876 878 def clearrebased(ui, repo, state, skipped, collapsedas=None):
877 879 """dispose of rebased revision at the end of the rebase
878 880
879 881 If `collapsedas` is not None, the rebase was a collapse whose result if the
880 882 `collapsedas` node."""
881 883 if obsolete._enabled:
882 884 markers = []
883 885 for rev, newrev in sorted(state.items()):
884 886 if newrev >= 0:
885 887 if rev in skipped:
886 888 succs = ()
887 889 elif collapsedas is not None:
888 890 succs = (repo[collapsedas],)
889 891 else:
890 892 succs = (repo[newrev],)
891 893 markers.append((repo[rev], succs))
892 894 if markers:
893 895 obsolete.createmarkers(repo, markers)
894 896 else:
895 897 rebased = [rev for rev in state if state[rev] > nullmerge]
896 898 if rebased:
897 899 stripped = []
898 900 for root in repo.set('roots(%ld)', rebased):
899 901 if set(repo.changelog.descendants([root.rev()])) - set(state):
900 902 ui.warn(_("warning: new changesets detected "
901 903 "on source branch, not stripping\n"))
902 904 else:
903 905 stripped.append(root.node())
904 906 if stripped:
905 907 # backup the old csets by default
906 908 repair.strip(ui, repo, stripped, "all")
907 909
908 910
909 911 def pullrebase(orig, ui, repo, *args, **opts):
910 912 'Call rebase after pull if the latter has been invoked with --rebase'
911 913 if opts.get('rebase'):
912 914 if opts.get('update'):
913 915 del opts['update']
914 916 ui.debug('--update and --rebase are not compatible, ignoring '
915 917 'the update flag\n')
916 918
917 919 movemarkfrom = repo['.'].node()
918 920 revsprepull = len(repo)
919 921 origpostincoming = commands.postincoming
920 922 def _dummy(*args, **kwargs):
921 923 pass
922 924 commands.postincoming = _dummy
923 925 try:
924 926 orig(ui, repo, *args, **opts)
925 927 finally:
926 928 commands.postincoming = origpostincoming
927 929 revspostpull = len(repo)
928 930 if revspostpull > revsprepull:
929 931 # --rev option from pull conflict with rebase own --rev
930 932 # dropping it
931 933 if 'rev' in opts:
932 934 del opts['rev']
933 935 rebase(ui, repo, **opts)
934 936 branch = repo[None].branch()
935 937 dest = repo[branch].rev()
936 938 if dest != repo['.'].rev():
937 939 # there was nothing to rebase we force an update
938 940 hg.update(repo, dest)
939 941 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
940 942 ui.status(_("updating bookmark %s\n")
941 943 % repo._bookmarkcurrent)
942 944 else:
943 945 if opts.get('tool'):
944 946 raise util.Abort(_('--tool can only be used with --rebase'))
945 947 orig(ui, repo, *args, **opts)
946 948
947 949 def summaryhook(ui, repo):
948 950 if not os.path.exists(repo.join('rebasestate')):
949 951 return
950 952 try:
951 953 state = restorestatus(repo)[2]
952 954 except error.RepoLookupError:
953 955 # i18n: column positioning for "hg summary"
954 956 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
955 957 ui.write(msg)
956 958 return
957 959 numrebased = len([i for i in state.itervalues() if i != -1])
958 960 # i18n: column positioning for "hg summary"
959 961 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
960 962 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
961 963 ui.label(_('%d remaining'), 'rebase.remaining') %
962 964 (len(state) - numrebased)))
963 965
964 966 def uisetup(ui):
965 967 'Replace pull with a decorator to provide --rebase option'
966 968 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
967 969 entry[1].append(('', 'rebase', None,
968 970 _("rebase working directory to branch head")))
969 971 entry[1].append(('t', 'tool', '',
970 972 _("specify merge tool for rebase")))
971 973 cmdutil.summaryhooks.add('rebase', summaryhook)
972 974 cmdutil.unfinishedstates.append(
973 975 ['rebasestate', False, False, _('rebase in progress'),
974 976 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
@@ -1,776 +1,781 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > rebase=
4 4 > mq=
5 5 >
6 6 > [phases]
7 7 > publish=False
8 8 >
9 9 > [alias]
10 10 > tglog = log -G --template "{rev}: '{desc}' {branches}\n"
11 11 > tglogp = log -G --template "{rev}:{phase} '{desc}' {branches}\n"
12 12 > EOF
13 13
14 14 Create repo a:
15 15
16 16 $ hg init a
17 17 $ cd a
18 18 $ hg unbundle "$TESTDIR/bundles/rebase.hg"
19 19 adding changesets
20 20 adding manifests
21 21 adding file changes
22 22 added 8 changesets with 7 changes to 7 files (+2 heads)
23 23 (run 'hg heads' to see heads, 'hg merge' to merge)
24 24 $ hg up tip
25 25 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
26 26
27 27 $ hg tglog
28 28 @ 7: 'H'
29 29 |
30 30 | o 6: 'G'
31 31 |/|
32 32 o | 5: 'F'
33 33 | |
34 34 | o 4: 'E'
35 35 |/
36 36 | o 3: 'D'
37 37 | |
38 38 | o 2: 'C'
39 39 | |
40 40 | o 1: 'B'
41 41 |/
42 42 o 0: 'A'
43 43
44 44 $ cd ..
45 45
46 46
47 47 Rebasing B onto H and collapsing changesets with different phases:
48 48
49 49
50 50 $ hg clone -q -u 3 a a1
51 51 $ cd a1
52 52
53 53 $ hg phase --force --secret 3
54 54
55 55 $ cat > $TESTTMP/editor.sh <<EOF
56 56 > echo "==== before editing"
57 57 > cat \$1
58 58 > echo "===="
59 59 > echo "edited manually" >> \$1
60 60 > EOF
61 61 $ HGEDITOR="sh $TESTTMP/editor.sh" hg rebase --collapse --keepbranches -e
62 62 ==== before editing
63 63 Collapsed revision
64 64 * B
65 65 * C
66 66 * D
67 67
68 68
69 69 HG: Enter commit message. Lines beginning with 'HG:' are removed.
70 70 HG: Leave message empty to abort commit.
71 71 HG: --
72 72 HG: user: Nicolas Dumazet <nicdumz.commits@gmail.com>
73 73 HG: branch 'default'
74 74 HG: changed B
75 75 HG: changed C
76 76 HG: changed D
77 77 ====
78 78 saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
79 79
80 80 $ hg tglogp
81 81 @ 5:secret 'Collapsed revision
82 82 | * B
83 83 | * C
84 84 | * D
85 85 |
86 86 |
87 87 | edited manually'
88 88 o 4:draft 'H'
89 89 |
90 90 | o 3:draft 'G'
91 91 |/|
92 92 o | 2:draft 'F'
93 93 | |
94 94 | o 1:draft 'E'
95 95 |/
96 96 o 0:draft 'A'
97 97
98 98 $ hg manifest --rev tip
99 99 A
100 100 B
101 101 C
102 102 D
103 103 F
104 104 H
105 105
106 106 $ cd ..
107 107
108 108
109 109 Rebasing E onto H:
110 110
111 111 $ hg clone -q -u . a a2
112 112 $ cd a2
113 113
114 114 $ hg phase --force --secret 6
115 115 $ hg rebase --source 4 --collapse
116 116 saved backup bundle to $TESTTMP/a2/.hg/strip-backup/*-backup.hg (glob)
117 117
118 118 $ hg tglog
119 119 o 6: 'Collapsed revision
120 120 | * E
121 121 | * G'
122 122 @ 5: 'H'
123 123 |
124 124 o 4: 'F'
125 125 |
126 126 | o 3: 'D'
127 127 | |
128 128 | o 2: 'C'
129 129 | |
130 130 | o 1: 'B'
131 131 |/
132 132 o 0: 'A'
133 133
134 134 $ hg manifest --rev tip
135 135 A
136 136 E
137 137 F
138 138 H
139 139
140 140 $ cd ..
141 141
142 142 Rebasing G onto H with custom message:
143 143
144 144 $ hg clone -q -u . a a3
145 145 $ cd a3
146 146
147 147 $ hg rebase --base 6 -m 'custom message'
148 148 abort: message can only be specified with collapse
149 149 [255]
150 150
151 $ hg rebase --source 4 --collapse -m 'custom message'
151 $ cat > $TESTTMP/checkeditform.sh <<EOF
152 > env | grep HGEDITFORM
153 > true
154 > EOF
155 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg rebase --source 4 --collapse -m 'custom message' -e
156 HGEDITFORM=rebase.collapse
152 157 saved backup bundle to $TESTTMP/a3/.hg/strip-backup/*-backup.hg (glob)
153 158
154 159 $ hg tglog
155 160 o 6: 'custom message'
156 161 |
157 162 @ 5: 'H'
158 163 |
159 164 o 4: 'F'
160 165 |
161 166 | o 3: 'D'
162 167 | |
163 168 | o 2: 'C'
164 169 | |
165 170 | o 1: 'B'
166 171 |/
167 172 o 0: 'A'
168 173
169 174 $ hg manifest --rev tip
170 175 A
171 176 E
172 177 F
173 178 H
174 179
175 180 $ cd ..
176 181
177 182 Create repo b:
178 183
179 184 $ hg init b
180 185 $ cd b
181 186
182 187 $ echo A > A
183 188 $ hg ci -Am A
184 189 adding A
185 190 $ echo B > B
186 191 $ hg ci -Am B
187 192 adding B
188 193
189 194 $ hg up -q 0
190 195
191 196 $ echo C > C
192 197 $ hg ci -Am C
193 198 adding C
194 199 created new head
195 200
196 201 $ hg merge
197 202 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
198 203 (branch merge, don't forget to commit)
199 204
200 205 $ echo D > D
201 206 $ hg ci -Am D
202 207 adding D
203 208
204 209 $ hg up -q 1
205 210
206 211 $ echo E > E
207 212 $ hg ci -Am E
208 213 adding E
209 214 created new head
210 215
211 216 $ echo F > F
212 217 $ hg ci -Am F
213 218 adding F
214 219
215 220 $ hg merge
216 221 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
217 222 (branch merge, don't forget to commit)
218 223 $ hg ci -m G
219 224
220 225 $ hg up -q 0
221 226
222 227 $ echo H > H
223 228 $ hg ci -Am H
224 229 adding H
225 230 created new head
226 231
227 232 $ hg tglog
228 233 @ 7: 'H'
229 234 |
230 235 | o 6: 'G'
231 236 | |\
232 237 | | o 5: 'F'
233 238 | | |
234 239 | | o 4: 'E'
235 240 | | |
236 241 | o | 3: 'D'
237 242 | |\|
238 243 | o | 2: 'C'
239 244 |/ /
240 245 | o 1: 'B'
241 246 |/
242 247 o 0: 'A'
243 248
244 249 $ cd ..
245 250
246 251
247 252 Rebase and collapse - more than one external (fail):
248 253
249 254 $ hg clone -q -u . b b1
250 255 $ cd b1
251 256
252 257 $ hg rebase -s 2 --collapse
253 258 abort: unable to collapse on top of 7, there is more than one external parent: 1, 5
254 259 [255]
255 260
256 261 Rebase and collapse - E onto H:
257 262
258 263 $ hg rebase -s 4 --collapse # root (4) is not a merge
259 264 saved backup bundle to $TESTTMP/b1/.hg/strip-backup/*-backup.hg (glob)
260 265
261 266 $ hg tglog
262 267 o 5: 'Collapsed revision
263 268 |\ * E
264 269 | | * F
265 270 | | * G'
266 271 | @ 4: 'H'
267 272 | |
268 273 o | 3: 'D'
269 274 |\ \
270 275 | o | 2: 'C'
271 276 | |/
272 277 o / 1: 'B'
273 278 |/
274 279 o 0: 'A'
275 280
276 281 $ hg manifest --rev tip
277 282 A
278 283 C
279 284 D
280 285 E
281 286 F
282 287 H
283 288
284 289 $ cd ..
285 290
286 291
287 292
288 293
289 294 Test that branchheads cache is updated correctly when doing a strip in which
290 295 the parent of the ancestor node to be stripped does not become a head and also,
291 296 the parent of a node that is a child of the node stripped becomes a head (node
292 297 3). The code is now much simpler and we could just test a simpler scenario
293 298 We keep it the test this way in case new complexity is injected.
294 299
295 300 $ hg clone -q -u . b b2
296 301 $ cd b2
297 302
298 303 $ hg heads --template="{rev}:{node} {branch}\n"
299 304 7:c65502d4178782309ce0574c5ae6ee9485a9bafa default
300 305 6:c772a8b2dc17629cec88a19d09c926c4814b12c7 default
301 306
302 307 $ cat $TESTTMP/b2/.hg/cache/branch2-served
303 308 c65502d4178782309ce0574c5ae6ee9485a9bafa 7
304 309 c772a8b2dc17629cec88a19d09c926c4814b12c7 o default
305 310 c65502d4178782309ce0574c5ae6ee9485a9bafa o default
306 311
307 312 $ hg strip 4
308 313 saved backup bundle to $TESTTMP/b2/.hg/strip-backup/8a5212ebc852-backup.hg (glob)
309 314
310 315 $ cat $TESTTMP/b2/.hg/cache/branch2-served
311 316 c65502d4178782309ce0574c5ae6ee9485a9bafa 4
312 317 2870ad076e541e714f3c2bc32826b5c6a6e5b040 o default
313 318 c65502d4178782309ce0574c5ae6ee9485a9bafa o default
314 319
315 320 $ hg heads --template="{rev}:{node} {branch}\n"
316 321 4:c65502d4178782309ce0574c5ae6ee9485a9bafa default
317 322 3:2870ad076e541e714f3c2bc32826b5c6a6e5b040 default
318 323
319 324 $ cd ..
320 325
321 326
322 327
323 328
324 329
325 330
326 331 Create repo c:
327 332
328 333 $ hg init c
329 334 $ cd c
330 335
331 336 $ echo A > A
332 337 $ hg ci -Am A
333 338 adding A
334 339 $ echo B > B
335 340 $ hg ci -Am B
336 341 adding B
337 342
338 343 $ hg up -q 0
339 344
340 345 $ echo C > C
341 346 $ hg ci -Am C
342 347 adding C
343 348 created new head
344 349
345 350 $ hg merge
346 351 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
347 352 (branch merge, don't forget to commit)
348 353
349 354 $ echo D > D
350 355 $ hg ci -Am D
351 356 adding D
352 357
353 358 $ hg up -q 1
354 359
355 360 $ echo E > E
356 361 $ hg ci -Am E
357 362 adding E
358 363 created new head
359 364 $ echo F > E
360 365 $ hg ci -m 'F'
361 366
362 367 $ echo G > G
363 368 $ hg ci -Am G
364 369 adding G
365 370
366 371 $ hg merge
367 372 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
368 373 (branch merge, don't forget to commit)
369 374
370 375 $ hg ci -m H
371 376
372 377 $ hg up -q 0
373 378
374 379 $ echo I > I
375 380 $ hg ci -Am I
376 381 adding I
377 382 created new head
378 383
379 384 $ hg tglog
380 385 @ 8: 'I'
381 386 |
382 387 | o 7: 'H'
383 388 | |\
384 389 | | o 6: 'G'
385 390 | | |
386 391 | | o 5: 'F'
387 392 | | |
388 393 | | o 4: 'E'
389 394 | | |
390 395 | o | 3: 'D'
391 396 | |\|
392 397 | o | 2: 'C'
393 398 |/ /
394 399 | o 1: 'B'
395 400 |/
396 401 o 0: 'A'
397 402
398 403 $ cd ..
399 404
400 405
401 406 Rebase and collapse - E onto I:
402 407
403 408 $ hg clone -q -u . c c1
404 409 $ cd c1
405 410
406 411 $ hg rebase -s 4 --collapse # root (4) is not a merge
407 412 merging E
408 413 saved backup bundle to $TESTTMP/c1/.hg/strip-backup/*-backup.hg (glob)
409 414
410 415 $ hg tglog
411 416 o 5: 'Collapsed revision
412 417 |\ * E
413 418 | | * F
414 419 | | * G
415 420 | | * H'
416 421 | @ 4: 'I'
417 422 | |
418 423 o | 3: 'D'
419 424 |\ \
420 425 | o | 2: 'C'
421 426 | |/
422 427 o / 1: 'B'
423 428 |/
424 429 o 0: 'A'
425 430
426 431 $ hg manifest --rev tip
427 432 A
428 433 C
429 434 D
430 435 E
431 436 G
432 437 I
433 438
434 439 $ hg up tip -q
435 440 $ cat E
436 441 F
437 442
438 443 $ cd ..
439 444
440 445
441 446 Create repo d:
442 447
443 448 $ hg init d
444 449 $ cd d
445 450
446 451 $ echo A > A
447 452 $ hg ci -Am A
448 453 adding A
449 454 $ echo B > B
450 455 $ hg ci -Am B
451 456 adding B
452 457 $ echo C > C
453 458 $ hg ci -Am C
454 459 adding C
455 460
456 461 $ hg up -q 1
457 462
458 463 $ echo D > D
459 464 $ hg ci -Am D
460 465 adding D
461 466 created new head
462 467 $ hg merge
463 468 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
464 469 (branch merge, don't forget to commit)
465 470
466 471 $ hg ci -m E
467 472
468 473 $ hg up -q 0
469 474
470 475 $ echo F > F
471 476 $ hg ci -Am F
472 477 adding F
473 478 created new head
474 479
475 480 $ hg tglog
476 481 @ 5: 'F'
477 482 |
478 483 | o 4: 'E'
479 484 | |\
480 485 | | o 3: 'D'
481 486 | | |
482 487 | o | 2: 'C'
483 488 | |/
484 489 | o 1: 'B'
485 490 |/
486 491 o 0: 'A'
487 492
488 493 $ cd ..
489 494
490 495
491 496 Rebase and collapse - B onto F:
492 497
493 498 $ hg clone -q -u . d d1
494 499 $ cd d1
495 500
496 501 $ hg rebase -s 1 --collapse
497 502 saved backup bundle to $TESTTMP/d1/.hg/strip-backup/*-backup.hg (glob)
498 503
499 504 $ hg tglog
500 505 o 2: 'Collapsed revision
501 506 | * B
502 507 | * C
503 508 | * D
504 509 | * E'
505 510 @ 1: 'F'
506 511 |
507 512 o 0: 'A'
508 513
509 514 $ hg manifest --rev tip
510 515 A
511 516 B
512 517 C
513 518 D
514 519 F
515 520
516 521 Interactions between collapse and keepbranches
517 522 $ cd ..
518 523 $ hg init e
519 524 $ cd e
520 525 $ echo 'a' > a
521 526 $ hg ci -Am 'A'
522 527 adding a
523 528
524 529 $ hg branch 'one'
525 530 marked working directory as branch one
526 531 (branches are permanent and global, did you want a bookmark?)
527 532 $ echo 'b' > b
528 533 $ hg ci -Am 'B'
529 534 adding b
530 535
531 536 $ hg branch 'two'
532 537 marked working directory as branch two
533 538 (branches are permanent and global, did you want a bookmark?)
534 539 $ echo 'c' > c
535 540 $ hg ci -Am 'C'
536 541 adding c
537 542
538 543 $ hg up -q 0
539 544 $ echo 'd' > d
540 545 $ hg ci -Am 'D'
541 546 adding d
542 547
543 548 $ hg tglog
544 549 @ 3: 'D'
545 550 |
546 551 | o 2: 'C' two
547 552 | |
548 553 | o 1: 'B' one
549 554 |/
550 555 o 0: 'A'
551 556
552 557 $ hg rebase --keepbranches --collapse -s 1 -d 3
553 558 abort: cannot collapse multiple named branches
554 559 [255]
555 560
556 561 $ repeatchange() {
557 562 > hg checkout $1
558 563 > hg cp d z
559 564 > echo blah >> z
560 565 > hg commit -Am "$2" --user "$3"
561 566 > }
562 567 $ repeatchange 3 "E" "user1"
563 568 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
564 569 $ repeatchange 3 "E" "user2"
565 570 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
566 571 created new head
567 572 $ hg tglog
568 573 @ 5: 'E'
569 574 |
570 575 | o 4: 'E'
571 576 |/
572 577 o 3: 'D'
573 578 |
574 579 | o 2: 'C' two
575 580 | |
576 581 | o 1: 'B' one
577 582 |/
578 583 o 0: 'A'
579 584
580 585 $ hg rebase -s 5 -d 4
581 586 saved backup bundle to $TESTTMP/e/.hg/strip-backup/*-backup.hg (glob)
582 587 $ hg tglog
583 588 @ 4: 'E'
584 589 |
585 590 o 3: 'D'
586 591 |
587 592 | o 2: 'C' two
588 593 | |
589 594 | o 1: 'B' one
590 595 |/
591 596 o 0: 'A'
592 597
593 598 $ hg export tip
594 599 # HG changeset patch
595 600 # User user1
596 601 # Date 0 0
597 602 # Thu Jan 01 00:00:00 1970 +0000
598 603 # Node ID f338eb3c2c7cc5b5915676a2376ba7ac558c5213
599 604 # Parent 41acb9dca9eb976e84cd21fcb756b4afa5a35c09
600 605 E
601 606
602 607 diff -r 41acb9dca9eb -r f338eb3c2c7c z
603 608 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
604 609 +++ b/z Thu Jan 01 00:00:00 1970 +0000
605 610 @@ -0,0 +1,2 @@
606 611 +d
607 612 +blah
608 613
609 614 $ cd ..
610 615
611 616 Rebase, collapse and copies
612 617
613 618 $ hg init copies
614 619 $ cd copies
615 620 $ hg unbundle "$TESTDIR/bundles/renames.hg"
616 621 adding changesets
617 622 adding manifests
618 623 adding file changes
619 624 added 4 changesets with 11 changes to 7 files (+1 heads)
620 625 (run 'hg heads' to see heads, 'hg merge' to merge)
621 626 $ hg up -q tip
622 627 $ hg tglog
623 628 @ 3: 'move2'
624 629 |
625 630 o 2: 'move1'
626 631 |
627 632 | o 1: 'change'
628 633 |/
629 634 o 0: 'add'
630 635
631 636 $ hg rebase --collapse -d 1
632 637 merging a and d to d
633 638 merging b and e to e
634 639 merging c and f to f
635 640 merging f and c to c
636 641 merging e and g to g
637 642 saved backup bundle to $TESTTMP/copies/.hg/strip-backup/*-backup.hg (glob)
638 643 $ hg st
639 644 $ hg st --copies --change tip
640 645 A d
641 646 a
642 647 A g
643 648 b
644 649 R b
645 650 $ hg up tip -q
646 651 $ cat c
647 652 c
648 653 c
649 654 $ cat d
650 655 a
651 656 a
652 657 $ cat g
653 658 b
654 659 b
655 660 $ hg log -r . --template "{file_copies}\n"
656 661 d (a)g (b)
657 662
658 663 Test collapsing a middle revision in-place
659 664
660 665 $ hg tglog
661 666 @ 2: 'Collapsed revision
662 667 | * move1
663 668 | * move2'
664 669 o 1: 'change'
665 670 |
666 671 o 0: 'add'
667 672
668 673 $ hg rebase --collapse -r 1 -d 0
669 674 abort: can't remove original changesets with unrebased descendants
670 675 (use --keep to keep original changesets)
671 676 [255]
672 677
673 678 Test collapsing in place
674 679
675 680 $ hg rebase --collapse -b . -d 0
676 681 saved backup bundle to $TESTTMP/copies/.hg/strip-backup/*-backup.hg (glob)
677 682 $ hg st --change tip --copies
678 683 M a
679 684 M c
680 685 A d
681 686 a
682 687 A g
683 688 b
684 689 R b
685 690 $ hg up tip -q
686 691 $ cat a
687 692 a
688 693 a
689 694 $ cat c
690 695 c
691 696 c
692 697 $ cat d
693 698 a
694 699 a
695 700 $ cat g
696 701 b
697 702 b
698 703 $ cd ..
699 704
700 705
701 706 Test stripping a revision with another child
702 707
703 708 $ hg init f
704 709 $ cd f
705 710
706 711 $ echo A > A
707 712 $ hg ci -Am A
708 713 adding A
709 714 $ echo B > B
710 715 $ hg ci -Am B
711 716 adding B
712 717
713 718 $ hg up -q 0
714 719
715 720 $ echo C > C
716 721 $ hg ci -Am C
717 722 adding C
718 723 created new head
719 724
720 725 $ hg tglog
721 726 @ 2: 'C'
722 727 |
723 728 | o 1: 'B'
724 729 |/
725 730 o 0: 'A'
726 731
727 732
728 733
729 734 $ hg heads --template="{rev}:{node} {branch}: {desc}\n"
730 735 2:c5cefa58fd557f84b72b87f970135984337acbc5 default: C
731 736 1:27547f69f25460a52fff66ad004e58da7ad3fb56 default: B
732 737
733 738 $ hg strip 2
734 739 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
735 740 saved backup bundle to $TESTTMP/f/.hg/strip-backup/*-backup.hg (glob)
736 741
737 742 $ hg tglog
738 743 o 1: 'B'
739 744 |
740 745 @ 0: 'A'
741 746
742 747
743 748
744 749 $ hg heads --template="{rev}:{node} {branch}: {desc}\n"
745 750 1:27547f69f25460a52fff66ad004e58da7ad3fb56 default: B
746 751
747 752 $ cd ..
748 753
749 754 Test collapsing changes that add then remove a file
750 755
751 756 $ hg init collapseaddremove
752 757 $ cd collapseaddremove
753 758
754 759 $ touch base
755 760 $ hg commit -Am base
756 761 adding base
757 762 $ touch a
758 763 $ hg commit -Am a
759 764 adding a
760 765 $ hg rm a
761 766 $ touch b
762 767 $ hg commit -Am b
763 768 adding b
764 769 $ hg book foo
765 770 $ hg rebase -d 0 -r "1::2" --collapse -m collapsed
766 771 saved backup bundle to $TESTTMP/collapseaddremove/.hg/strip-backup/*-backup.hg (glob)
767 772 $ hg log -G --template "{rev}: '{desc}' {bookmarks}"
768 773 @ 1: 'collapsed' foo
769 774 |
770 775 o 0: 'base'
771 776
772 777 $ hg manifest --rev tip
773 778 b
774 779 base
775 780
776 781 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now