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