##// END OF EJS Templates
rebase: check no-op before checking phase (issue3891)...
Siddharth Agarwal -
r19059:53eadcb8 default
parent child Browse files
Show More
@@ -1,788 +1,788 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 @command('rebase',
33 33 [('s', 'source', '',
34 34 _('rebase from the specified changeset'), _('REV')),
35 35 ('b', 'base', '',
36 36 _('rebase from the base of the specified changeset '
37 37 '(up to greatest common ancestor of base and dest)'),
38 38 _('REV')),
39 39 ('r', 'rev', [],
40 40 _('rebase these revisions'),
41 41 _('REV')),
42 42 ('d', 'dest', '',
43 43 _('rebase onto the specified changeset'), _('REV')),
44 44 ('', 'collapse', False, _('collapse the rebased changesets')),
45 45 ('m', 'message', '',
46 46 _('use text as collapse commit message'), _('TEXT')),
47 47 ('e', 'edit', False, _('invoke editor on commit messages')),
48 48 ('l', 'logfile', '',
49 49 _('read collapse commit message from file'), _('FILE')),
50 50 ('', 'keep', False, _('keep original changesets')),
51 51 ('', 'keepbranches', False, _('keep original branch names')),
52 52 ('D', 'detach', False, _('(DEPRECATED)')),
53 53 ('t', 'tool', '', _('specify merge tool')),
54 54 ('c', 'continue', False, _('continue an interrupted rebase')),
55 55 ('a', 'abort', False, _('abort an interrupted rebase'))] +
56 56 templateopts,
57 57 _('[-s REV | -b REV] [-d REV] [OPTION]'))
58 58 def rebase(ui, repo, **opts):
59 59 """move changeset (and descendants) to a different branch
60 60
61 61 Rebase uses repeated merging to graft changesets from one part of
62 62 history (the source) onto another (the destination). This can be
63 63 useful for linearizing *local* changes relative to a master
64 64 development tree.
65 65
66 66 You should not rebase changesets that have already been shared
67 67 with others. Doing so will force everybody else to perform the
68 68 same rebase or they will end up with duplicated changesets after
69 69 pulling in your rebased changesets.
70 70
71 71 In its default configuration, Mercurial will prevent you from
72 72 rebasing published changes. See :hg:`help phases` for details.
73 73
74 74 If you don't specify a destination changeset (``-d/--dest``),
75 75 rebase uses the tipmost head of the current named branch as the
76 76 destination. (The destination changeset is not modified by
77 77 rebasing, but new changesets are added as its descendants.)
78 78
79 79 You can specify which changesets to rebase in two ways: as a
80 80 "source" changeset or as a "base" changeset. Both are shorthand
81 81 for a topologically related set of changesets (the "source
82 82 branch"). If you specify source (``-s/--source``), rebase will
83 83 rebase that changeset and all of its descendants onto dest. If you
84 84 specify base (``-b/--base``), rebase will select ancestors of base
85 85 back to but not including the common ancestor with dest. Thus,
86 86 ``-b`` is less precise but more convenient than ``-s``: you can
87 87 specify any changeset in the source branch, and rebase will select
88 88 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
89 89 uses the parent of the working directory as the base.
90 90
91 91 For advanced usage, a third way is available through the ``--rev``
92 92 option. It allows you to specify an arbitrary set of changesets to
93 93 rebase. Descendants of revs you specify with this option are not
94 94 automatically included in the rebase.
95 95
96 96 By default, rebase recreates the changesets in the source branch
97 97 as descendants of dest and then destroys the originals. Use
98 98 ``--keep`` to preserve the original source changesets. Some
99 99 changesets in the source branch (e.g. merges from the destination
100 100 branch) may be dropped if they no longer contribute any change.
101 101
102 102 One result of the rules for selecting the destination changeset
103 103 and source branch is that, unlike ``merge``, rebase will do
104 104 nothing if you are at the latest (tipmost) head of a named branch
105 105 with two heads. You need to explicitly specify source and/or
106 106 destination (or ``update`` to the other head, if it's the head of
107 107 the intended source branch).
108 108
109 109 If a rebase is interrupted to manually resolve a merge, it can be
110 110 continued with --continue/-c or aborted with --abort/-a.
111 111
112 112 Returns 0 on success, 1 if nothing to rebase.
113 113 """
114 114 originalwd = target = None
115 115 activebookmark = None
116 116 external = nullrev
117 117 state = {}
118 118 skipped = set()
119 119 targetancestors = set()
120 120
121 121 editor = None
122 122 if opts.get('edit'):
123 123 editor = cmdutil.commitforceeditor
124 124
125 125 lock = wlock = None
126 126 try:
127 127 wlock = repo.wlock()
128 128 lock = repo.lock()
129 129
130 130 # Validate input and define rebasing points
131 131 destf = opts.get('dest', None)
132 132 srcf = opts.get('source', None)
133 133 basef = opts.get('base', None)
134 134 revf = opts.get('rev', [])
135 135 contf = opts.get('continue')
136 136 abortf = opts.get('abort')
137 137 collapsef = opts.get('collapse', False)
138 138 collapsemsg = cmdutil.logmessage(ui, opts)
139 139 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
140 140 keepf = opts.get('keep', False)
141 141 keepbranchesf = opts.get('keepbranches', False)
142 142 # keepopen is not meant for use on the command line, but by
143 143 # other extensions
144 144 keepopen = opts.get('keepopen', False)
145 145
146 146 if collapsemsg and not collapsef:
147 147 raise util.Abort(
148 148 _('message can only be specified with collapse'))
149 149
150 150 if contf or abortf:
151 151 if contf and abortf:
152 152 raise util.Abort(_('cannot use both abort and continue'))
153 153 if collapsef:
154 154 raise util.Abort(
155 155 _('cannot use collapse with continue or abort'))
156 156 if srcf or basef or destf:
157 157 raise util.Abort(
158 158 _('abort and continue do not allow specifying revisions'))
159 159 if opts.get('tool', False):
160 160 ui.warn(_('tool option will be ignored\n'))
161 161
162 162 (originalwd, target, state, skipped, collapsef, keepf,
163 163 keepbranchesf, external, activebookmark) = restorestatus(repo)
164 164 if abortf:
165 165 return abort(repo, originalwd, target, state)
166 166 else:
167 167 if srcf and basef:
168 168 raise util.Abort(_('cannot specify both a '
169 169 'source and a base'))
170 170 if revf and basef:
171 171 raise util.Abort(_('cannot specify both a '
172 172 'revision and a base'))
173 173 if revf and srcf:
174 174 raise util.Abort(_('cannot specify both a '
175 175 'revision and a source'))
176 176
177 177 cmdutil.bailifchanged(repo)
178 178
179 179 if not destf:
180 180 # Destination defaults to the latest revision in the
181 181 # current branch
182 182 branch = repo[None].branch()
183 183 dest = repo[branch]
184 184 else:
185 185 dest = scmutil.revsingle(repo, destf)
186 186
187 187 if revf:
188 188 rebaseset = repo.revs('%lr', revf)
189 189 elif srcf:
190 190 src = scmutil.revrange(repo, [srcf])
191 191 rebaseset = repo.revs('(%ld)::', src)
192 192 else:
193 193 base = scmutil.revrange(repo, [basef or '.'])
194 194 rebaseset = repo.revs(
195 195 '(children(ancestor(%ld, %d)) and ::(%ld))::',
196 196 base, dest, base)
197 197 if rebaseset:
198 198 root = min(rebaseset)
199 199 else:
200 200 root = None
201 201
202 202 if not rebaseset:
203 203 repo.ui.debug('base is ancestor of destination\n')
204 204 result = None
205 205 elif (not (keepf or obsolete._enabled)
206 206 and repo.revs('first(children(%ld) - %ld)',
207 207 rebaseset, rebaseset)):
208 208 raise util.Abort(
209 209 _("can't remove original changesets with"
210 210 " unrebased descendants"),
211 211 hint=_('use --keep to keep original changesets'))
212 elif not keepf and not repo[root].mutable():
213 raise util.Abort(_("can't rebase immutable changeset %s")
214 % repo[root],
215 hint=_('see hg help phases for details'))
216 212 else:
217 213 result = buildstate(repo, dest, rebaseset, collapsef)
218 214
219 215 if not result:
220 216 # Empty state built, nothing to rebase
221 217 ui.status(_('nothing to rebase\n'))
222 218 return 1
219 elif not keepf and not repo[root].mutable():
220 raise util.Abort(_("can't rebase immutable changeset %s")
221 % repo[root],
222 hint=_('see hg help phases for details'))
223 223 else:
224 224 originalwd, target, state = result
225 225 if collapsef:
226 226 targetancestors = repo.changelog.ancestors([target],
227 227 inclusive=True)
228 228 external = checkexternal(repo, state, targetancestors)
229 229
230 230 if keepbranchesf:
231 231 assert not extrafn, 'cannot use both keepbranches and extrafn'
232 232 def extrafn(ctx, extra):
233 233 extra['branch'] = ctx.branch()
234 234 if collapsef:
235 235 branches = set()
236 236 for rev in state:
237 237 branches.add(repo[rev].branch())
238 238 if len(branches) > 1:
239 239 raise util.Abort(_('cannot collapse multiple named '
240 240 'branches'))
241 241
242 242
243 243 # Rebase
244 244 if not targetancestors:
245 245 targetancestors = repo.changelog.ancestors([target], inclusive=True)
246 246
247 247 # Keep track of the current bookmarks in order to reset them later
248 248 currentbookmarks = repo._bookmarks.copy()
249 249 activebookmark = activebookmark or repo._bookmarkcurrent
250 250 if activebookmark:
251 251 bookmarks.unsetcurrent(repo)
252 252
253 253 sortedstate = sorted(state)
254 254 total = len(sortedstate)
255 255 pos = 0
256 256 for rev in sortedstate:
257 257 pos += 1
258 258 if state[rev] == -1:
259 259 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
260 260 _('changesets'), total)
261 261 storestatus(repo, originalwd, target, state, collapsef, keepf,
262 262 keepbranchesf, external, activebookmark)
263 263 p1, p2 = defineparents(repo, rev, target, state,
264 264 targetancestors)
265 265 if len(repo.parents()) == 2:
266 266 repo.ui.debug('resuming interrupted rebase\n')
267 267 else:
268 268 try:
269 269 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
270 270 stats = rebasenode(repo, rev, p1, state, collapsef)
271 271 if stats and stats[3] > 0:
272 272 raise error.InterventionRequired(
273 273 _('unresolved conflicts (see hg '
274 274 'resolve, then hg rebase --continue)'))
275 275 finally:
276 276 ui.setconfig('ui', 'forcemerge', '')
277 277 cmdutil.duplicatecopies(repo, rev, target)
278 278 if not collapsef:
279 279 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
280 280 editor=editor)
281 281 else:
282 282 # Skip commit if we are collapsing
283 283 repo.setparents(repo[p1].node())
284 284 newrev = None
285 285 # Update the state
286 286 if newrev is not None:
287 287 state[rev] = repo[newrev].rev()
288 288 else:
289 289 if not collapsef:
290 290 ui.note(_('no changes, revision %d skipped\n') % rev)
291 291 ui.debug('next revision set to %s\n' % p1)
292 292 skipped.add(rev)
293 293 state[rev] = p1
294 294
295 295 ui.progress(_('rebasing'), None)
296 296 ui.note(_('rebase merging completed\n'))
297 297
298 298 if collapsef and not keepopen:
299 299 p1, p2 = defineparents(repo, min(state), target,
300 300 state, targetancestors)
301 301 if collapsemsg:
302 302 commitmsg = collapsemsg
303 303 else:
304 304 commitmsg = 'Collapsed revision'
305 305 for rebased in state:
306 306 if rebased not in skipped and state[rebased] > nullmerge:
307 307 commitmsg += '\n* %s' % repo[rebased].description()
308 308 commitmsg = ui.edit(commitmsg, repo.ui.username())
309 309 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
310 310 extrafn=extrafn, editor=editor)
311 311
312 312 if 'qtip' in repo.tags():
313 313 updatemq(repo, state, skipped, **opts)
314 314
315 315 if currentbookmarks:
316 316 # Nodeids are needed to reset bookmarks
317 317 nstate = {}
318 318 for k, v in state.iteritems():
319 319 if v > nullmerge:
320 320 nstate[repo[k].node()] = repo[v].node()
321 321 # XXX this is the same as dest.node() for the non-continue path --
322 322 # this should probably be cleaned up
323 323 targetnode = repo[target].node()
324 324
325 325 if not keepf:
326 326 collapsedas = None
327 327 if collapsef:
328 328 collapsedas = newrev
329 329 clearrebased(ui, repo, state, skipped, collapsedas)
330 330
331 331 if currentbookmarks:
332 332 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
333 333
334 334 clearstatus(repo)
335 335 ui.note(_("rebase completed\n"))
336 336 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
337 337 if skipped:
338 338 ui.note(_("%d revisions have been skipped\n") % len(skipped))
339 339
340 340 if (activebookmark and
341 341 repo['tip'].node() == repo._bookmarks[activebookmark]):
342 342 bookmarks.setcurrent(repo, activebookmark)
343 343
344 344 finally:
345 345 release(lock, wlock)
346 346
347 347 def checkexternal(repo, state, targetancestors):
348 348 """Check whether one or more external revisions need to be taken in
349 349 consideration. In the latter case, abort.
350 350 """
351 351 external = nullrev
352 352 source = min(state)
353 353 for rev in state:
354 354 if rev == source:
355 355 continue
356 356 # Check externals and fail if there are more than one
357 357 for p in repo[rev].parents():
358 358 if (p.rev() not in state
359 359 and p.rev() not in targetancestors):
360 360 if external != nullrev:
361 361 raise util.Abort(_('unable to collapse, there is more '
362 362 'than one external parent'))
363 363 external = p.rev()
364 364 return external
365 365
366 366 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
367 367 'Commit the changes and store useful information in extra'
368 368 try:
369 369 repo.setparents(repo[p1].node(), repo[p2].node())
370 370 ctx = repo[rev]
371 371 if commitmsg is None:
372 372 commitmsg = ctx.description()
373 373 extra = {'rebase_source': ctx.hex()}
374 374 if extrafn:
375 375 extrafn(ctx, extra)
376 376 # Commit might fail if unresolved files exist
377 377 newrev = repo.commit(text=commitmsg, user=ctx.user(),
378 378 date=ctx.date(), extra=extra, editor=editor)
379 379 repo.dirstate.setbranch(repo[newrev].branch())
380 380 targetphase = max(ctx.phase(), phases.draft)
381 381 # retractboundary doesn't overwrite upper phase inherited from parent
382 382 newnode = repo[newrev].node()
383 383 if newnode:
384 384 phases.retractboundary(repo, targetphase, [newnode])
385 385 return newrev
386 386 except util.Abort:
387 387 # Invalidate the previous setparents
388 388 repo.dirstate.invalidate()
389 389 raise
390 390
391 391 def rebasenode(repo, rev, p1, state, collapse):
392 392 'Rebase a single revision'
393 393 # Merge phase
394 394 # Update to target and merge it with local
395 395 if repo['.'].rev() != repo[p1].rev():
396 396 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
397 397 merge.update(repo, p1, False, True, False)
398 398 else:
399 399 repo.ui.debug(" already in target\n")
400 400 repo.dirstate.write()
401 401 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
402 402 base = None
403 403 if repo[rev].rev() != repo[min(state)].rev():
404 404 base = repo[rev].p1().node()
405 405 # When collapsing in-place, the parent is the common ancestor, we
406 406 # have to allow merging with it.
407 407 return merge.update(repo, rev, True, True, False, base, collapse)
408 408
409 409 def nearestrebased(repo, rev, state):
410 410 """return the nearest ancestors of rev in the rebase result"""
411 411 rebased = [r for r in state if state[r] > nullmerge]
412 412 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
413 413 if candidates:
414 414 return state[candidates[0]]
415 415 else:
416 416 return None
417 417
418 418 def defineparents(repo, rev, target, state, targetancestors):
419 419 'Return the new parent relationship of the revision that will be rebased'
420 420 parents = repo[rev].parents()
421 421 p1 = p2 = nullrev
422 422
423 423 P1n = parents[0].rev()
424 424 if P1n in targetancestors:
425 425 p1 = target
426 426 elif P1n in state:
427 427 if state[P1n] == nullmerge:
428 428 p1 = target
429 429 elif state[P1n] == revignored:
430 430 p1 = nearestrebased(repo, P1n, state)
431 431 if p1 is None:
432 432 p1 = target
433 433 else:
434 434 p1 = state[P1n]
435 435 else: # P1n external
436 436 p1 = target
437 437 p2 = P1n
438 438
439 439 if len(parents) == 2 and parents[1].rev() not in targetancestors:
440 440 P2n = parents[1].rev()
441 441 # interesting second parent
442 442 if P2n in state:
443 443 if p1 == target: # P1n in targetancestors or external
444 444 p1 = state[P2n]
445 445 elif state[P2n] == revignored:
446 446 p2 = nearestrebased(repo, P2n, state)
447 447 if p2 is None:
448 448 # no ancestors rebased yet, detach
449 449 p2 = target
450 450 else:
451 451 p2 = state[P2n]
452 452 else: # P2n external
453 453 if p2 != nullrev: # P1n external too => rev is a merged revision
454 454 raise util.Abort(_('cannot use revision %d as base, result '
455 455 'would have 3 parents') % rev)
456 456 p2 = P2n
457 457 repo.ui.debug(" future parents are %d and %d\n" %
458 458 (repo[p1].rev(), repo[p2].rev()))
459 459 return p1, p2
460 460
461 461 def isagitpatch(repo, patchname):
462 462 'Return true if the given patch is in git format'
463 463 mqpatch = os.path.join(repo.mq.path, patchname)
464 464 for line in patch.linereader(file(mqpatch, 'rb')):
465 465 if line.startswith('diff --git'):
466 466 return True
467 467 return False
468 468
469 469 def updatemq(repo, state, skipped, **opts):
470 470 'Update rebased mq patches - finalize and then import them'
471 471 mqrebase = {}
472 472 mq = repo.mq
473 473 original_series = mq.fullseries[:]
474 474 skippedpatches = set()
475 475
476 476 for p in mq.applied:
477 477 rev = repo[p.node].rev()
478 478 if rev in state:
479 479 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
480 480 (rev, p.name))
481 481 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
482 482 else:
483 483 # Applied but not rebased, not sure this should happen
484 484 skippedpatches.add(p.name)
485 485
486 486 if mqrebase:
487 487 mq.finish(repo, mqrebase.keys())
488 488
489 489 # We must start import from the newest revision
490 490 for rev in sorted(mqrebase, reverse=True):
491 491 if rev not in skipped:
492 492 name, isgit = mqrebase[rev]
493 493 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
494 494 mq.qimport(repo, (), patchname=name, git=isgit,
495 495 rev=[str(state[rev])])
496 496 else:
497 497 # Rebased and skipped
498 498 skippedpatches.add(mqrebase[rev][0])
499 499
500 500 # Patches were either applied and rebased and imported in
501 501 # order, applied and removed or unapplied. Discard the removed
502 502 # ones while preserving the original series order and guards.
503 503 newseries = [s for s in original_series
504 504 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
505 505 mq.fullseries[:] = newseries
506 506 mq.seriesdirty = True
507 507 mq.savedirty()
508 508
509 509 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
510 510 'Move bookmarks to their correct changesets, and delete divergent ones'
511 511 marks = repo._bookmarks
512 512 for k, v in originalbookmarks.iteritems():
513 513 if v in nstate:
514 514 # update the bookmarks for revs that have moved
515 515 marks[k] = nstate[v]
516 516 bookmarks.deletedivergent(repo, [targetnode], k)
517 517
518 518 marks.write()
519 519
520 520 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
521 521 external, activebookmark):
522 522 'Store the current status to allow recovery'
523 523 f = repo.opener("rebasestate", "w")
524 524 f.write(repo[originalwd].hex() + '\n')
525 525 f.write(repo[target].hex() + '\n')
526 526 f.write(repo[external].hex() + '\n')
527 527 f.write('%d\n' % int(collapse))
528 528 f.write('%d\n' % int(keep))
529 529 f.write('%d\n' % int(keepbranches))
530 530 f.write('%s\n' % (activebookmark or ''))
531 531 for d, v in state.iteritems():
532 532 oldrev = repo[d].hex()
533 533 if v > nullmerge:
534 534 newrev = repo[v].hex()
535 535 else:
536 536 newrev = v
537 537 f.write("%s:%s\n" % (oldrev, newrev))
538 538 f.close()
539 539 repo.ui.debug('rebase status stored\n')
540 540
541 541 def clearstatus(repo):
542 542 'Remove the status files'
543 543 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
544 544
545 545 def restorestatus(repo):
546 546 'Restore a previously stored status'
547 547 try:
548 548 target = None
549 549 collapse = False
550 550 external = nullrev
551 551 activebookmark = None
552 552 state = {}
553 553 f = repo.opener("rebasestate")
554 554 for i, l in enumerate(f.read().splitlines()):
555 555 if i == 0:
556 556 originalwd = repo[l].rev()
557 557 elif i == 1:
558 558 target = repo[l].rev()
559 559 elif i == 2:
560 560 external = repo[l].rev()
561 561 elif i == 3:
562 562 collapse = bool(int(l))
563 563 elif i == 4:
564 564 keep = bool(int(l))
565 565 elif i == 5:
566 566 keepbranches = bool(int(l))
567 567 elif i == 6 and not (len(l) == 81 and ':' in l):
568 568 # line 6 is a recent addition, so for backwards compatibility
569 569 # check that the line doesn't look like the oldrev:newrev lines
570 570 activebookmark = l
571 571 else:
572 572 oldrev, newrev = l.split(':')
573 573 if newrev in (str(nullmerge), str(revignored)):
574 574 state[repo[oldrev].rev()] = int(newrev)
575 575 else:
576 576 state[repo[oldrev].rev()] = repo[newrev].rev()
577 577 skipped = set()
578 578 # recompute the set of skipped revs
579 579 if not collapse:
580 580 seen = set([target])
581 581 for old, new in sorted(state.items()):
582 582 if new != nullrev and new in seen:
583 583 skipped.add(old)
584 584 seen.add(new)
585 585 repo.ui.debug('computed skipped revs: %s\n' % skipped)
586 586 repo.ui.debug('rebase status resumed\n')
587 587 return (originalwd, target, state, skipped,
588 588 collapse, keep, keepbranches, external, activebookmark)
589 589 except IOError, err:
590 590 if err.errno != errno.ENOENT:
591 591 raise
592 592 raise util.Abort(_('no rebase in progress'))
593 593
594 594 def abort(repo, originalwd, target, state):
595 595 'Restore the repository to its original state'
596 596 dstates = [s for s in state.values() if s != nullrev]
597 597 immutable = [d for d in dstates if not repo[d].mutable()]
598 598 if immutable:
599 599 raise util.Abort(_("can't abort rebase due to immutable changesets %s")
600 600 % ', '.join(str(repo[r]) for r in immutable),
601 601 hint=_('see hg help phases for details'))
602 602
603 603 descendants = set()
604 604 if dstates:
605 605 descendants = set(repo.changelog.descendants(dstates))
606 606 if descendants - set(dstates):
607 607 repo.ui.warn(_("warning: new changesets detected on target branch, "
608 608 "can't abort\n"))
609 609 return -1
610 610 else:
611 611 # Strip from the first rebased revision
612 612 merge.update(repo, repo[originalwd].rev(), False, True, False)
613 613 rebased = filter(lambda x: x > -1 and x != target, state.values())
614 614 if rebased:
615 615 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
616 616 # no backup of rebased cset versions needed
617 617 repair.strip(repo.ui, repo, strippoints)
618 618 clearstatus(repo)
619 619 repo.ui.warn(_('rebase aborted\n'))
620 620 return 0
621 621
622 622 def buildstate(repo, dest, rebaseset, collapse):
623 623 '''Define which revisions are going to be rebased and where
624 624
625 625 repo: repo
626 626 dest: context
627 627 rebaseset: set of rev
628 628 '''
629 629
630 630 # This check isn't strictly necessary, since mq detects commits over an
631 631 # applied patch. But it prevents messing up the working directory when
632 632 # a partially completed rebase is blocked by mq.
633 633 if 'qtip' in repo.tags() and (dest.node() in
634 634 [s.node for s in repo.mq.applied]):
635 635 raise util.Abort(_('cannot rebase onto an applied mq patch'))
636 636
637 637 roots = list(repo.set('roots(%ld)', rebaseset))
638 638 if not roots:
639 639 raise util.Abort(_('no matching revisions'))
640 640 roots.sort()
641 641 state = {}
642 642 detachset = set()
643 643 for root in roots:
644 644 commonbase = root.ancestor(dest)
645 645 if commonbase == root:
646 646 raise util.Abort(_('source is ancestor of destination'))
647 647 if commonbase == dest:
648 648 samebranch = root.branch() == dest.branch()
649 649 if not collapse and samebranch and root in dest.children():
650 650 repo.ui.debug('source is a child of destination\n')
651 651 return None
652 652
653 653 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))
654 654 state.update(dict.fromkeys(rebaseset, nullrev))
655 655 # Rebase tries to turn <dest> into a parent of <root> while
656 656 # preserving the number of parents of rebased changesets:
657 657 #
658 658 # - A changeset with a single parent will always be rebased as a
659 659 # changeset with a single parent.
660 660 #
661 661 # - A merge will be rebased as merge unless its parents are both
662 662 # ancestors of <dest> or are themselves in the rebased set and
663 663 # pruned while rebased.
664 664 #
665 665 # If one parent of <root> is an ancestor of <dest>, the rebased
666 666 # version of this parent will be <dest>. This is always true with
667 667 # --base option.
668 668 #
669 669 # Otherwise, we need to *replace* the original parents with
670 670 # <dest>. This "detaches" the rebased set from its former location
671 671 # and rebases it onto <dest>. Changes introduced by ancestors of
672 672 # <root> not common with <dest> (the detachset, marked as
673 673 # nullmerge) are "removed" from the rebased changesets.
674 674 #
675 675 # - If <root> has a single parent, set it to <dest>.
676 676 #
677 677 # - If <root> is a merge, we cannot decide which parent to
678 678 # replace, the rebase operation is not clearly defined.
679 679 #
680 680 # The table below sums up this behavior:
681 681 #
682 682 # +------------------+----------------------+-------------------------+
683 683 # | | one parent | merge |
684 684 # +------------------+----------------------+-------------------------+
685 685 # | parent in | new parent is <dest> | parents in ::<dest> are |
686 686 # | ::<dest> | | remapped to <dest> |
687 687 # +------------------+----------------------+-------------------------+
688 688 # | unrelated source | new parent is <dest> | ambiguous, abort |
689 689 # +------------------+----------------------+-------------------------+
690 690 #
691 691 # The actual abort is handled by `defineparents`
692 692 if len(root.parents()) <= 1:
693 693 # ancestors of <root> not ancestors of <dest>
694 694 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
695 695 [root.rev()]))
696 696 for r in detachset:
697 697 if r not in state:
698 698 state[r] = nullmerge
699 699 if len(roots) > 1:
700 700 # If we have multiple roots, we may have "hole" in the rebase set.
701 701 # Rebase roots that descend from those "hole" should not be detached as
702 702 # other root are. We use the special `revignored` to inform rebase that
703 703 # the revision should be ignored but that `defineparents` should search
704 704 # a rebase destination that make sense regarding rebased topology.
705 705 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
706 706 for ignored in set(rebasedomain) - set(rebaseset):
707 707 state[ignored] = revignored
708 708 return repo['.'].rev(), dest.rev(), state
709 709
710 710 def clearrebased(ui, repo, state, skipped, collapsedas=None):
711 711 """dispose of rebased revision at the end of the rebase
712 712
713 713 If `collapsedas` is not None, the rebase was a collapse whose result if the
714 714 `collapsedas` node."""
715 715 if obsolete._enabled:
716 716 markers = []
717 717 for rev, newrev in sorted(state.items()):
718 718 if newrev >= 0:
719 719 if rev in skipped:
720 720 succs = ()
721 721 elif collapsedas is not None:
722 722 succs = (repo[collapsedas],)
723 723 else:
724 724 succs = (repo[newrev],)
725 725 markers.append((repo[rev], succs))
726 726 if markers:
727 727 obsolete.createmarkers(repo, markers)
728 728 else:
729 729 rebased = [rev for rev in state if state[rev] > nullmerge]
730 730 if rebased:
731 731 stripped = []
732 732 for root in repo.set('roots(%ld)', rebased):
733 733 if set(repo.changelog.descendants([root.rev()])) - set(state):
734 734 ui.warn(_("warning: new changesets detected "
735 735 "on source branch, not stripping\n"))
736 736 else:
737 737 stripped.append(root.node())
738 738 if stripped:
739 739 # backup the old csets by default
740 740 repair.strip(ui, repo, stripped, "all")
741 741
742 742
743 743 def pullrebase(orig, ui, repo, *args, **opts):
744 744 'Call rebase after pull if the latter has been invoked with --rebase'
745 745 if opts.get('rebase'):
746 746 if opts.get('update'):
747 747 del opts['update']
748 748 ui.debug('--update and --rebase are not compatible, ignoring '
749 749 'the update flag\n')
750 750
751 751 movemarkfrom = repo['.'].node()
752 752 cmdutil.bailifchanged(repo)
753 753 revsprepull = len(repo)
754 754 origpostincoming = commands.postincoming
755 755 def _dummy(*args, **kwargs):
756 756 pass
757 757 commands.postincoming = _dummy
758 758 try:
759 759 orig(ui, repo, *args, **opts)
760 760 finally:
761 761 commands.postincoming = origpostincoming
762 762 revspostpull = len(repo)
763 763 if revspostpull > revsprepull:
764 764 # --rev option from pull conflict with rebase own --rev
765 765 # dropping it
766 766 if 'rev' in opts:
767 767 del opts['rev']
768 768 rebase(ui, repo, **opts)
769 769 branch = repo[None].branch()
770 770 dest = repo[branch].rev()
771 771 if dest != repo['.'].rev():
772 772 # there was nothing to rebase we force an update
773 773 hg.update(repo, dest)
774 774 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
775 775 ui.status(_("updating bookmark %s\n")
776 776 % repo._bookmarkcurrent)
777 777 else:
778 778 if opts.get('tool'):
779 779 raise util.Abort(_('--tool can only be used with --rebase'))
780 780 orig(ui, repo, *args, **opts)
781 781
782 782 def uisetup(ui):
783 783 'Replace pull with a decorator to provide --rebase option'
784 784 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
785 785 entry[1].append(('', 'rebase', None,
786 786 _("rebase working directory to branch head")))
787 787 entry[1].append(('t', 'tool', '',
788 788 _("specify merge tool for rebase")))
@@ -1,649 +1,652 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > graphlog=
4 4 > rebase=
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 > EOF
12 12
13 13
14 14 $ hg init a
15 15 $ cd a
16 16 $ hg unbundle "$TESTDIR/bundles/rebase.hg"
17 17 adding changesets
18 18 adding manifests
19 19 adding file changes
20 20 added 8 changesets with 7 changes to 7 files (+2 heads)
21 21 (run 'hg heads' to see heads, 'hg merge' to merge)
22 22 $ hg up tip
23 23 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
24 24 $ cd ..
25 25
26 26
27 27 Rebasing
28 28 D onto H - simple rebase:
29 29
30 30 $ hg clone -q -u . a a1
31 31 $ cd a1
32 32
33 33 $ hg tglog
34 34 @ 7: 'H'
35 35 |
36 36 | o 6: 'G'
37 37 |/|
38 38 o | 5: 'F'
39 39 | |
40 40 | o 4: 'E'
41 41 |/
42 42 | o 3: 'D'
43 43 | |
44 44 | o 2: 'C'
45 45 | |
46 46 | o 1: 'B'
47 47 |/
48 48 o 0: 'A'
49 49
50 50
51 51 $ hg rebase -s 3 -d 7
52 52 saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
53 53
54 54 $ hg tglog
55 55 @ 7: 'D'
56 56 |
57 57 o 6: 'H'
58 58 |
59 59 | o 5: 'G'
60 60 |/|
61 61 o | 4: 'F'
62 62 | |
63 63 | o 3: 'E'
64 64 |/
65 65 | o 2: 'C'
66 66 | |
67 67 | o 1: 'B'
68 68 |/
69 69 o 0: 'A'
70 70
71 71 $ cd ..
72 72
73 73
74 74 D onto F - intermediate point:
75 75
76 76 $ hg clone -q -u . a a2
77 77 $ cd a2
78 78
79 79 $ hg rebase -s 3 -d 5
80 80 saved backup bundle to $TESTTMP/a2/.hg/strip-backup/*-backup.hg (glob)
81 81
82 82 $ hg tglog
83 83 @ 7: 'D'
84 84 |
85 85 | o 6: 'H'
86 86 |/
87 87 | o 5: 'G'
88 88 |/|
89 89 o | 4: 'F'
90 90 | |
91 91 | o 3: 'E'
92 92 |/
93 93 | o 2: 'C'
94 94 | |
95 95 | o 1: 'B'
96 96 |/
97 97 o 0: 'A'
98 98
99 99 $ cd ..
100 100
101 101
102 102 E onto H - skip of G:
103 103
104 104 $ hg clone -q -u . a a3
105 105 $ cd a3
106 106
107 107 $ hg rebase -s 4 -d 7
108 108 saved backup bundle to $TESTTMP/a3/.hg/strip-backup/*-backup.hg (glob)
109 109
110 110 $ hg tglog
111 111 @ 6: 'E'
112 112 |
113 113 o 5: 'H'
114 114 |
115 115 o 4: 'F'
116 116 |
117 117 | o 3: 'D'
118 118 | |
119 119 | o 2: 'C'
120 120 | |
121 121 | o 1: 'B'
122 122 |/
123 123 o 0: 'A'
124 124
125 125 $ cd ..
126 126
127 127
128 128 F onto E - rebase of a branching point (skip G):
129 129
130 130 $ hg clone -q -u . a a4
131 131 $ cd a4
132 132
133 133 $ hg rebase -s 5 -d 4
134 134 saved backup bundle to $TESTTMP/a4/.hg/strip-backup/*-backup.hg (glob)
135 135
136 136 $ hg tglog
137 137 @ 6: 'H'
138 138 |
139 139 o 5: 'F'
140 140 |
141 141 o 4: 'E'
142 142 |
143 143 | o 3: 'D'
144 144 | |
145 145 | o 2: 'C'
146 146 | |
147 147 | o 1: 'B'
148 148 |/
149 149 o 0: 'A'
150 150
151 151 $ cd ..
152 152
153 153
154 154 G onto H - merged revision having a parent in ancestors of target:
155 155
156 156 $ hg clone -q -u . a a5
157 157 $ cd a5
158 158
159 159 $ hg rebase -s 6 -d 7
160 160 saved backup bundle to $TESTTMP/a5/.hg/strip-backup/*-backup.hg (glob)
161 161
162 162 $ hg tglog
163 163 @ 7: 'G'
164 164 |\
165 165 | o 6: 'H'
166 166 | |
167 167 | o 5: 'F'
168 168 | |
169 169 o | 4: 'E'
170 170 |/
171 171 | o 3: 'D'
172 172 | |
173 173 | o 2: 'C'
174 174 | |
175 175 | o 1: 'B'
176 176 |/
177 177 o 0: 'A'
178 178
179 179 $ cd ..
180 180
181 181
182 182 F onto B - G maintains E as parent:
183 183
184 184 $ hg clone -q -u . a a6
185 185 $ cd a6
186 186
187 187 $ hg rebase -s 5 -d 1
188 188 saved backup bundle to $TESTTMP/a6/.hg/strip-backup/*-backup.hg (glob)
189 189
190 190 $ hg tglog
191 191 @ 7: 'H'
192 192 |
193 193 | o 6: 'G'
194 194 |/|
195 195 o | 5: 'F'
196 196 | |
197 197 | o 4: 'E'
198 198 | |
199 199 | | o 3: 'D'
200 200 | | |
201 201 +---o 2: 'C'
202 202 | |
203 203 o | 1: 'B'
204 204 |/
205 205 o 0: 'A'
206 206
207 207 $ cd ..
208 208
209 209
210 210 These will fail (using --source):
211 211
212 212 G onto F - rebase onto an ancestor:
213 213
214 214 $ hg clone -q -u . a a7
215 215 $ cd a7
216 216
217 217 $ hg rebase -s 6 -d 5
218 218 nothing to rebase
219 219 [1]
220 220
221 221 F onto G - rebase onto a descendant:
222 222
223 223 $ hg rebase -s 5 -d 6
224 224 abort: source is ancestor of destination
225 225 [255]
226 226
227 227 G onto B - merge revision with both parents not in ancestors of target:
228 228
229 229 $ hg rebase -s 6 -d 1
230 230 abort: cannot use revision 6 as base, result would have 3 parents
231 231 [255]
232 232
233 233
234 234 These will abort gracefully (using --base):
235 235
236 236 G onto G - rebase onto same changeset:
237 237
238 238 $ hg rebase -b 6 -d 6
239 239 nothing to rebase
240 240 [1]
241 241
242 242 G onto F - rebase onto an ancestor:
243 243
244 244 $ hg rebase -b 6 -d 5
245 245 nothing to rebase
246 246 [1]
247 247
248 248 F onto G - rebase onto a descendant:
249 249
250 250 $ hg rebase -b 5 -d 6
251 251 nothing to rebase
252 252 [1]
253 253
254 254 C onto A - rebase onto an ancestor:
255 255
256 256 $ hg rebase -d 0 -s 2
257 257 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/5fddd98957c8-backup.hg (glob)
258 258 $ hg tglog
259 259 @ 7: 'D'
260 260 |
261 261 o 6: 'C'
262 262 |
263 263 | o 5: 'H'
264 264 | |
265 265 | | o 4: 'G'
266 266 | |/|
267 267 | o | 3: 'F'
268 268 |/ /
269 269 | o 2: 'E'
270 270 |/
271 271 | o 1: 'B'
272 272 |/
273 273 o 0: 'A'
274 274
275 275
276 276 Check rebasing public changeset
277 277
278 278 $ hg pull --config phases.publish=True -q -r 6 . # update phase of 6
279 $ hg rebase -d 0 -b 6
280 nothing to rebase
281 [1]
279 282 $ hg rebase -d 5 -b 6
280 283 abort: can't rebase immutable changeset e1c4361dd923
281 284 (see hg help phases for details)
282 285 [255]
283 286
284 287 $ hg rebase -d 5 -b 6 --keep
285 288
286 289 Check rebasing mutable changeset
287 290 Source phase greater or equal to destination phase: new changeset get the phase of source:
288 291 $ hg rebase -s9 -d0
289 292 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/2b23e52411f4-backup.hg (glob)
290 293 $ hg log --template "{phase}\n" -r 9
291 294 draft
292 295 $ hg rebase -s9 -d1
293 296 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/2cb10d0cfc6c-backup.hg (glob)
294 297 $ hg log --template "{phase}\n" -r 9
295 298 draft
296 299 $ hg phase --force --secret 9
297 300 $ hg rebase -s9 -d0
298 301 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/c5b12b67163a-backup.hg (glob)
299 302 $ hg log --template "{phase}\n" -r 9
300 303 secret
301 304 $ hg rebase -s9 -d1
302 305 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/2a0524f868ac-backup.hg (glob)
303 306 $ hg log --template "{phase}\n" -r 9
304 307 secret
305 308 Source phase lower than destination phase: new changeset get the phase of destination:
306 309 $ hg rebase -s8 -d9
307 310 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/6d4f22462821-backup.hg (glob)
308 311 $ hg log --template "{phase}\n" -r 'rev(9)'
309 312 secret
310 313
311 314 $ cd ..
312 315
313 316 Test for revset
314 317
315 318 We need a bit different graph
316 319 All destination are B
317 320
318 321 $ hg init ah
319 322 $ cd ah
320 323 $ hg unbundle "$TESTDIR/bundles/rebase-revset.hg"
321 324 adding changesets
322 325 adding manifests
323 326 adding file changes
324 327 added 9 changesets with 9 changes to 9 files (+2 heads)
325 328 (run 'hg heads' to see heads, 'hg merge' to merge)
326 329 $ hg tglog
327 330 o 8: 'I'
328 331 |
329 332 o 7: 'H'
330 333 |
331 334 o 6: 'G'
332 335 |
333 336 | o 5: 'F'
334 337 | |
335 338 | o 4: 'E'
336 339 |/
337 340 o 3: 'D'
338 341 |
339 342 o 2: 'C'
340 343 |
341 344 | o 1: 'B'
342 345 |/
343 346 o 0: 'A'
344 347
345 348 $ cd ..
346 349
347 350
348 351 Simple case with keep:
349 352
350 353 Source on have two descendant heads but ask for one
351 354
352 355 $ hg clone -q -u . ah ah1
353 356 $ cd ah1
354 357 $ hg rebase -r '2::8' -d 1
355 358 abort: can't remove original changesets with unrebased descendants
356 359 (use --keep to keep original changesets)
357 360 [255]
358 361 $ hg rebase -r '2::8' -d 1 --keep
359 362 $ hg tglog
360 363 @ 13: 'I'
361 364 |
362 365 o 12: 'H'
363 366 |
364 367 o 11: 'G'
365 368 |
366 369 o 10: 'D'
367 370 |
368 371 o 9: 'C'
369 372 |
370 373 | o 8: 'I'
371 374 | |
372 375 | o 7: 'H'
373 376 | |
374 377 | o 6: 'G'
375 378 | |
376 379 | | o 5: 'F'
377 380 | | |
378 381 | | o 4: 'E'
379 382 | |/
380 383 | o 3: 'D'
381 384 | |
382 385 | o 2: 'C'
383 386 | |
384 387 o | 1: 'B'
385 388 |/
386 389 o 0: 'A'
387 390
388 391
389 392 $ cd ..
390 393
391 394 Base on have one descendant heads we ask for but common ancestor have two
392 395
393 396 $ hg clone -q -u . ah ah2
394 397 $ cd ah2
395 398 $ hg rebase -r '3::8' -d 1
396 399 abort: can't remove original changesets with unrebased descendants
397 400 (use --keep to keep original changesets)
398 401 [255]
399 402 $ hg rebase -r '3::8' -d 1 --keep
400 403 $ hg tglog
401 404 @ 12: 'I'
402 405 |
403 406 o 11: 'H'
404 407 |
405 408 o 10: 'G'
406 409 |
407 410 o 9: 'D'
408 411 |
409 412 | o 8: 'I'
410 413 | |
411 414 | o 7: 'H'
412 415 | |
413 416 | o 6: 'G'
414 417 | |
415 418 | | o 5: 'F'
416 419 | | |
417 420 | | o 4: 'E'
418 421 | |/
419 422 | o 3: 'D'
420 423 | |
421 424 | o 2: 'C'
422 425 | |
423 426 o | 1: 'B'
424 427 |/
425 428 o 0: 'A'
426 429
427 430
428 431 $ cd ..
429 432
430 433 rebase subset
431 434
432 435 $ hg clone -q -u . ah ah3
433 436 $ cd ah3
434 437 $ hg rebase -r '3::7' -d 1
435 438 abort: can't remove original changesets with unrebased descendants
436 439 (use --keep to keep original changesets)
437 440 [255]
438 441 $ hg rebase -r '3::7' -d 1 --keep
439 442 $ hg tglog
440 443 @ 11: 'H'
441 444 |
442 445 o 10: 'G'
443 446 |
444 447 o 9: 'D'
445 448 |
446 449 | o 8: 'I'
447 450 | |
448 451 | o 7: 'H'
449 452 | |
450 453 | o 6: 'G'
451 454 | |
452 455 | | o 5: 'F'
453 456 | | |
454 457 | | o 4: 'E'
455 458 | |/
456 459 | o 3: 'D'
457 460 | |
458 461 | o 2: 'C'
459 462 | |
460 463 o | 1: 'B'
461 464 |/
462 465 o 0: 'A'
463 466
464 467
465 468 $ cd ..
466 469
467 470 rebase subset with multiple head
468 471
469 472 $ hg clone -q -u . ah ah4
470 473 $ cd ah4
471 474 $ hg rebase -r '3::(7+5)' -d 1
472 475 abort: can't remove original changesets with unrebased descendants
473 476 (use --keep to keep original changesets)
474 477 [255]
475 478 $ hg rebase -r '3::(7+5)' -d 1 --keep
476 479 $ hg tglog
477 480 @ 13: 'H'
478 481 |
479 482 o 12: 'G'
480 483 |
481 484 | o 11: 'F'
482 485 | |
483 486 | o 10: 'E'
484 487 |/
485 488 o 9: 'D'
486 489 |
487 490 | o 8: 'I'
488 491 | |
489 492 | o 7: 'H'
490 493 | |
491 494 | o 6: 'G'
492 495 | |
493 496 | | o 5: 'F'
494 497 | | |
495 498 | | o 4: 'E'
496 499 | |/
497 500 | o 3: 'D'
498 501 | |
499 502 | o 2: 'C'
500 503 | |
501 504 o | 1: 'B'
502 505 |/
503 506 o 0: 'A'
504 507
505 508
506 509 $ cd ..
507 510
508 511 More advanced tests
509 512
510 513 rebase on ancestor with revset
511 514
512 515 $ hg clone -q -u . ah ah5
513 516 $ cd ah5
514 517 $ hg rebase -r '6::' -d 2
515 518 saved backup bundle to $TESTTMP/ah5/.hg/strip-backup/3d8a618087a7-backup.hg (glob)
516 519 $ hg tglog
517 520 @ 8: 'I'
518 521 |
519 522 o 7: 'H'
520 523 |
521 524 o 6: 'G'
522 525 |
523 526 | o 5: 'F'
524 527 | |
525 528 | o 4: 'E'
526 529 | |
527 530 | o 3: 'D'
528 531 |/
529 532 o 2: 'C'
530 533 |
531 534 | o 1: 'B'
532 535 |/
533 536 o 0: 'A'
534 537
535 538 $ cd ..
536 539
537 540
538 541 rebase with multiple root.
539 542 We rebase E and G on B
540 543 We would expect heads are I, F if it was supported
541 544
542 545 $ hg clone -q -u . ah ah6
543 546 $ cd ah6
544 547 $ hg rebase -r '(4+6)::' -d 1
545 548 saved backup bundle to $TESTTMP/ah6/.hg/strip-backup/3d8a618087a7-backup.hg (glob)
546 549 $ hg tglog
547 550 @ 8: 'I'
548 551 |
549 552 o 7: 'H'
550 553 |
551 554 o 6: 'G'
552 555 |
553 556 | o 5: 'F'
554 557 | |
555 558 | o 4: 'E'
556 559 |/
557 560 | o 3: 'D'
558 561 | |
559 562 | o 2: 'C'
560 563 | |
561 564 o | 1: 'B'
562 565 |/
563 566 o 0: 'A'
564 567
565 568 $ cd ..
566 569
567 570 More complex rebase with multiple roots
568 571 each root have a different common ancestor with the destination and this is a detach
569 572
570 573 (setup)
571 574
572 575 $ hg clone -q -u . a a8
573 576 $ cd a8
574 577 $ echo I > I
575 578 $ hg add I
576 579 $ hg commit -m I
577 580 $ hg up 4
578 581 1 files updated, 0 files merged, 3 files removed, 0 files unresolved
579 582 $ echo I > J
580 583 $ hg add J
581 584 $ hg commit -m J
582 585 created new head
583 586 $ echo I > K
584 587 $ hg add K
585 588 $ hg commit -m K
586 589 $ hg tglog
587 590 @ 10: 'K'
588 591 |
589 592 o 9: 'J'
590 593 |
591 594 | o 8: 'I'
592 595 | |
593 596 | o 7: 'H'
594 597 | |
595 598 +---o 6: 'G'
596 599 | |/
597 600 | o 5: 'F'
598 601 | |
599 602 o | 4: 'E'
600 603 |/
601 604 | o 3: 'D'
602 605 | |
603 606 | o 2: 'C'
604 607 | |
605 608 | o 1: 'B'
606 609 |/
607 610 o 0: 'A'
608 611
609 612 (actual test)
610 613
611 614 $ hg rebase --dest 'desc(G)' --rev 'desc(K) + desc(I)'
612 615 saved backup bundle to $TESTTMP/a8/.hg/strip-backup/23a4ace37988-backup.hg (glob)
613 616 $ hg log --rev 'children(desc(G))'
614 617 changeset: 9:adb617877056
615 618 parent: 6:eea13746799a
616 619 user: test
617 620 date: Thu Jan 01 00:00:00 1970 +0000
618 621 summary: I
619 622
620 623 changeset: 10:882431a34a0e
621 624 tag: tip
622 625 parent: 6:eea13746799a
623 626 user: test
624 627 date: Thu Jan 01 00:00:00 1970 +0000
625 628 summary: K
626 629
627 630 $ hg tglog
628 631 @ 10: 'K'
629 632 |
630 633 | o 9: 'I'
631 634 |/
632 635 | o 8: 'J'
633 636 | |
634 637 | | o 7: 'H'
635 638 | | |
636 639 o---+ 6: 'G'
637 640 |/ /
638 641 | o 5: 'F'
639 642 | |
640 643 o | 4: 'E'
641 644 |/
642 645 | o 3: 'D'
643 646 | |
644 647 | o 2: 'C'
645 648 | |
646 649 | o 1: 'B'
647 650 |/
648 651 o 0: 'A'
649 652
General Comments 0
You need to be logged in to leave comments. Login now