##// END OF EJS Templates
rebase: allow non-head rebase-set when obsolete is enabled...
Pierre-Yves David -
r18164:bacf55bd default
parent child Browse files
Show More
@@ -1,736 +1,737 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 # temporary top level filtering of extinct revisions
188 188 rebaseset = repo.revs('%ld - hidden()', rebaseset)
189 189 if rebaseset:
190 190 root = min(rebaseset)
191 191 else:
192 192 root = None
193 193
194 194 if not rebaseset:
195 195 repo.ui.debug('base is ancestor of destination\n')
196 196 result = None
197 elif not keepf and repo.revs('first(children(%ld) - %ld)-hidden()',
198 rebaseset, rebaseset):
197 elif (not (keepf or obsolete._enabled)
198 and repo.revs('first(children(%ld) - %ld)-hidden()',
199 rebaseset, rebaseset)):
199 200 raise util.Abort(
200 201 _("can't remove original changesets with"
201 202 " unrebased descendants"),
202 203 hint=_('use --keep to keep original changesets'))
203 204 elif not keepf and not repo[root].mutable():
204 205 raise util.Abort(_("can't rebase immutable changeset %s")
205 206 % repo[root],
206 207 hint=_('see hg help phases for details'))
207 208 else:
208 209 result = buildstate(repo, dest, rebaseset, collapsef)
209 210
210 211 if not result:
211 212 # Empty state built, nothing to rebase
212 213 ui.status(_('nothing to rebase\n'))
213 214 return 1
214 215 else:
215 216 originalwd, target, state = result
216 217 if collapsef:
217 218 targetancestors = repo.changelog.ancestors([target],
218 219 inclusive=True)
219 220 external = checkexternal(repo, state, targetancestors)
220 221
221 222 if keepbranchesf:
222 223 assert not extrafn, 'cannot use both keepbranches and extrafn'
223 224 def extrafn(ctx, extra):
224 225 extra['branch'] = ctx.branch()
225 226 if collapsef:
226 227 branches = set()
227 228 for rev in state:
228 229 branches.add(repo[rev].branch())
229 230 if len(branches) > 1:
230 231 raise util.Abort(_('cannot collapse multiple named '
231 232 'branches'))
232 233
233 234
234 235 # Rebase
235 236 if not targetancestors:
236 237 targetancestors = repo.changelog.ancestors([target], inclusive=True)
237 238
238 239 # Keep track of the current bookmarks in order to reset them later
239 240 currentbookmarks = repo._bookmarks.copy()
240 241 activebookmark = repo._bookmarkcurrent
241 242 if activebookmark:
242 243 bookmarks.unsetcurrent(repo)
243 244
244 245 sortedstate = sorted(state)
245 246 total = len(sortedstate)
246 247 pos = 0
247 248 for rev in sortedstate:
248 249 pos += 1
249 250 if state[rev] == -1:
250 251 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
251 252 _('changesets'), total)
252 253 storestatus(repo, originalwd, target, state, collapsef, keepf,
253 254 keepbranchesf, external)
254 255 p1, p2 = defineparents(repo, rev, target, state,
255 256 targetancestors)
256 257 if len(repo.parents()) == 2:
257 258 repo.ui.debug('resuming interrupted rebase\n')
258 259 else:
259 260 try:
260 261 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
261 262 stats = rebasenode(repo, rev, p1, state, collapsef)
262 263 if stats and stats[3] > 0:
263 264 raise util.Abort(_('unresolved conflicts (see hg '
264 265 'resolve, then hg rebase --continue)'))
265 266 finally:
266 267 ui.setconfig('ui', 'forcemerge', '')
267 268 cmdutil.duplicatecopies(repo, rev, target)
268 269 if not collapsef:
269 270 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
270 271 editor=editor)
271 272 else:
272 273 # Skip commit if we are collapsing
273 274 repo.setparents(repo[p1].node())
274 275 newrev = None
275 276 # Update the state
276 277 if newrev is not None:
277 278 state[rev] = repo[newrev].rev()
278 279 else:
279 280 if not collapsef:
280 281 ui.note(_('no changes, revision %d skipped\n') % rev)
281 282 ui.debug('next revision set to %s\n' % p1)
282 283 skipped.add(rev)
283 284 state[rev] = p1
284 285
285 286 ui.progress(_('rebasing'), None)
286 287 ui.note(_('rebase merging completed\n'))
287 288
288 289 if collapsef and not keepopen:
289 290 p1, p2 = defineparents(repo, min(state), target,
290 291 state, targetancestors)
291 292 if collapsemsg:
292 293 commitmsg = collapsemsg
293 294 else:
294 295 commitmsg = 'Collapsed revision'
295 296 for rebased in state:
296 297 if rebased not in skipped and state[rebased] != nullmerge:
297 298 commitmsg += '\n* %s' % repo[rebased].description()
298 299 commitmsg = ui.edit(commitmsg, repo.ui.username())
299 300 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
300 301 extrafn=extrafn, editor=editor)
301 302
302 303 if 'qtip' in repo.tags():
303 304 updatemq(repo, state, skipped, **opts)
304 305
305 306 if currentbookmarks:
306 307 # Nodeids are needed to reset bookmarks
307 308 nstate = {}
308 309 for k, v in state.iteritems():
309 310 if v != nullmerge:
310 311 nstate[repo[k].node()] = repo[v].node()
311 312
312 313 if not keepf:
313 314 collapsedas = None
314 315 if collapsef:
315 316 collapsedas = newrev
316 317 clearrebased(ui, repo, state, collapsedas)
317 318
318 319 if currentbookmarks:
319 320 updatebookmarks(repo, nstate, currentbookmarks, **opts)
320 321
321 322 clearstatus(repo)
322 323 ui.note(_("rebase completed\n"))
323 324 if os.path.exists(repo.sjoin('undo')):
324 325 util.unlinkpath(repo.sjoin('undo'))
325 326 if skipped:
326 327 ui.note(_("%d revisions have been skipped\n") % len(skipped))
327 328
328 329 if (activebookmark and
329 330 repo['tip'].node() == repo._bookmarks[activebookmark]):
330 331 bookmarks.setcurrent(repo, activebookmark)
331 332
332 333 finally:
333 334 release(lock, wlock)
334 335
335 336 def checkexternal(repo, state, targetancestors):
336 337 """Check whether one or more external revisions need to be taken in
337 338 consideration. In the latter case, abort.
338 339 """
339 340 external = nullrev
340 341 source = min(state)
341 342 for rev in state:
342 343 if rev == source:
343 344 continue
344 345 # Check externals and fail if there are more than one
345 346 for p in repo[rev].parents():
346 347 if (p.rev() not in state
347 348 and p.rev() not in targetancestors):
348 349 if external != nullrev:
349 350 raise util.Abort(_('unable to collapse, there is more '
350 351 'than one external parent'))
351 352 external = p.rev()
352 353 return external
353 354
354 355 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
355 356 'Commit the changes and store useful information in extra'
356 357 try:
357 358 repo.setparents(repo[p1].node(), repo[p2].node())
358 359 ctx = repo[rev]
359 360 if commitmsg is None:
360 361 commitmsg = ctx.description()
361 362 extra = {'rebase_source': ctx.hex()}
362 363 if extrafn:
363 364 extrafn(ctx, extra)
364 365 # Commit might fail if unresolved files exist
365 366 newrev = repo.commit(text=commitmsg, user=ctx.user(),
366 367 date=ctx.date(), extra=extra, editor=editor)
367 368 repo.dirstate.setbranch(repo[newrev].branch())
368 369 targetphase = max(ctx.phase(), phases.draft)
369 370 # retractboundary doesn't overwrite upper phase inherited from parent
370 371 newnode = repo[newrev].node()
371 372 if newnode:
372 373 phases.retractboundary(repo, targetphase, [newnode])
373 374 return newrev
374 375 except util.Abort:
375 376 # Invalidate the previous setparents
376 377 repo.dirstate.invalidate()
377 378 raise
378 379
379 380 def rebasenode(repo, rev, p1, state, collapse):
380 381 'Rebase a single revision'
381 382 # Merge phase
382 383 # Update to target and merge it with local
383 384 if repo['.'].rev() != repo[p1].rev():
384 385 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
385 386 merge.update(repo, p1, False, True, False)
386 387 else:
387 388 repo.ui.debug(" already in target\n")
388 389 repo.dirstate.write()
389 390 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
390 391 base = None
391 392 if repo[rev].rev() != repo[min(state)].rev():
392 393 base = repo[rev].p1().node()
393 394 # When collapsing in-place, the parent is the common ancestor, we
394 395 # have to allow merging with it.
395 396 return merge.update(repo, rev, True, True, False, base, collapse)
396 397
397 398 def defineparents(repo, rev, target, state, targetancestors):
398 399 'Return the new parent relationship of the revision that will be rebased'
399 400 parents = repo[rev].parents()
400 401 p1 = p2 = nullrev
401 402
402 403 P1n = parents[0].rev()
403 404 if P1n in targetancestors:
404 405 p1 = target
405 406 elif P1n in state:
406 407 if state[P1n] == nullmerge:
407 408 p1 = target
408 409 else:
409 410 p1 = state[P1n]
410 411 else: # P1n external
411 412 p1 = target
412 413 p2 = P1n
413 414
414 415 if len(parents) == 2 and parents[1].rev() not in targetancestors:
415 416 P2n = parents[1].rev()
416 417 # interesting second parent
417 418 if P2n in state:
418 419 if p1 == target: # P1n in targetancestors or external
419 420 p1 = state[P2n]
420 421 else:
421 422 p2 = state[P2n]
422 423 else: # P2n external
423 424 if p2 != nullrev: # P1n external too => rev is a merged revision
424 425 raise util.Abort(_('cannot use revision %d as base, result '
425 426 'would have 3 parents') % rev)
426 427 p2 = P2n
427 428 repo.ui.debug(" future parents are %d and %d\n" %
428 429 (repo[p1].rev(), repo[p2].rev()))
429 430 return p1, p2
430 431
431 432 def isagitpatch(repo, patchname):
432 433 'Return true if the given patch is in git format'
433 434 mqpatch = os.path.join(repo.mq.path, patchname)
434 435 for line in patch.linereader(file(mqpatch, 'rb')):
435 436 if line.startswith('diff --git'):
436 437 return True
437 438 return False
438 439
439 440 def updatemq(repo, state, skipped, **opts):
440 441 'Update rebased mq patches - finalize and then import them'
441 442 mqrebase = {}
442 443 mq = repo.mq
443 444 original_series = mq.fullseries[:]
444 445 skippedpatches = set()
445 446
446 447 for p in mq.applied:
447 448 rev = repo[p.node].rev()
448 449 if rev in state:
449 450 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
450 451 (rev, p.name))
451 452 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
452 453 else:
453 454 # Applied but not rebased, not sure this should happen
454 455 skippedpatches.add(p.name)
455 456
456 457 if mqrebase:
457 458 mq.finish(repo, mqrebase.keys())
458 459
459 460 # We must start import from the newest revision
460 461 for rev in sorted(mqrebase, reverse=True):
461 462 if rev not in skipped:
462 463 name, isgit = mqrebase[rev]
463 464 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
464 465 mq.qimport(repo, (), patchname=name, git=isgit,
465 466 rev=[str(state[rev])])
466 467 else:
467 468 # Rebased and skipped
468 469 skippedpatches.add(mqrebase[rev][0])
469 470
470 471 # Patches were either applied and rebased and imported in
471 472 # order, applied and removed or unapplied. Discard the removed
472 473 # ones while preserving the original series order and guards.
473 474 newseries = [s for s in original_series
474 475 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
475 476 mq.fullseries[:] = newseries
476 477 mq.seriesdirty = True
477 478 mq.savedirty()
478 479
479 480 def updatebookmarks(repo, nstate, originalbookmarks, **opts):
480 481 'Move bookmarks to their correct changesets'
481 482 marks = repo._bookmarks
482 483 for k, v in originalbookmarks.iteritems():
483 484 if v in nstate:
484 485 if nstate[v] != nullmerge:
485 486 # update the bookmarks for revs that have moved
486 487 marks[k] = nstate[v]
487 488
488 489 marks.write()
489 490
490 491 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
491 492 external):
492 493 'Store the current status to allow recovery'
493 494 f = repo.opener("rebasestate", "w")
494 495 f.write(repo[originalwd].hex() + '\n')
495 496 f.write(repo[target].hex() + '\n')
496 497 f.write(repo[external].hex() + '\n')
497 498 f.write('%d\n' % int(collapse))
498 499 f.write('%d\n' % int(keep))
499 500 f.write('%d\n' % int(keepbranches))
500 501 for d, v in state.iteritems():
501 502 oldrev = repo[d].hex()
502 503 if v != nullmerge:
503 504 newrev = repo[v].hex()
504 505 else:
505 506 newrev = v
506 507 f.write("%s:%s\n" % (oldrev, newrev))
507 508 f.close()
508 509 repo.ui.debug('rebase status stored\n')
509 510
510 511 def clearstatus(repo):
511 512 'Remove the status files'
512 513 if os.path.exists(repo.join("rebasestate")):
513 514 util.unlinkpath(repo.join("rebasestate"))
514 515
515 516 def restorestatus(repo):
516 517 'Restore a previously stored status'
517 518 try:
518 519 target = None
519 520 collapse = False
520 521 external = nullrev
521 522 state = {}
522 523 f = repo.opener("rebasestate")
523 524 for i, l in enumerate(f.read().splitlines()):
524 525 if i == 0:
525 526 originalwd = repo[l].rev()
526 527 elif i == 1:
527 528 target = repo[l].rev()
528 529 elif i == 2:
529 530 external = repo[l].rev()
530 531 elif i == 3:
531 532 collapse = bool(int(l))
532 533 elif i == 4:
533 534 keep = bool(int(l))
534 535 elif i == 5:
535 536 keepbranches = bool(int(l))
536 537 else:
537 538 oldrev, newrev = l.split(':')
538 539 if newrev != str(nullmerge):
539 540 state[repo[oldrev].rev()] = repo[newrev].rev()
540 541 else:
541 542 state[repo[oldrev].rev()] = int(newrev)
542 543 skipped = set()
543 544 # recompute the set of skipped revs
544 545 if not collapse:
545 546 seen = set([target])
546 547 for old, new in sorted(state.items()):
547 548 if new != nullrev and new in seen:
548 549 skipped.add(old)
549 550 seen.add(new)
550 551 repo.ui.debug('computed skipped revs: %s\n' % skipped)
551 552 repo.ui.debug('rebase status resumed\n')
552 553 return (originalwd, target, state, skipped,
553 554 collapse, keep, keepbranches, external)
554 555 except IOError, err:
555 556 if err.errno != errno.ENOENT:
556 557 raise
557 558 raise util.Abort(_('no rebase in progress'))
558 559
559 560 def abort(repo, originalwd, target, state):
560 561 'Restore the repository to its original state'
561 562 dstates = [s for s in state.values() if s != nullrev]
562 563 immutable = [d for d in dstates if not repo[d].mutable()]
563 564 if immutable:
564 565 raise util.Abort(_("can't abort rebase due to immutable changesets %s")
565 566 % ', '.join(str(repo[r]) for r in immutable),
566 567 hint=_('see hg help phases for details'))
567 568
568 569 descendants = set()
569 570 if dstates:
570 571 descendants = set(repo.changelog.descendants(dstates))
571 572 if descendants - set(dstates):
572 573 repo.ui.warn(_("warning: new changesets detected on target branch, "
573 574 "can't abort\n"))
574 575 return -1
575 576 else:
576 577 # Strip from the first rebased revision
577 578 merge.update(repo, repo[originalwd].rev(), False, True, False)
578 579 rebased = filter(lambda x: x > -1 and x != target, state.values())
579 580 if rebased:
580 581 strippoint = min(rebased)
581 582 # no backup of rebased cset versions needed
582 583 repair.strip(repo.ui, repo, repo[strippoint].node())
583 584 clearstatus(repo)
584 585 repo.ui.warn(_('rebase aborted\n'))
585 586 return 0
586 587
587 588 def buildstate(repo, dest, rebaseset, collapse):
588 589 '''Define which revisions are going to be rebased and where
589 590
590 591 repo: repo
591 592 dest: context
592 593 rebaseset: set of rev
593 594 '''
594 595
595 596 # This check isn't strictly necessary, since mq detects commits over an
596 597 # applied patch. But it prevents messing up the working directory when
597 598 # a partially completed rebase is blocked by mq.
598 599 if 'qtip' in repo.tags() and (dest.node() in
599 600 [s.node for s in repo.mq.applied]):
600 601 raise util.Abort(_('cannot rebase onto an applied mq patch'))
601 602
602 603 roots = list(repo.set('roots(%ld)', rebaseset))
603 604 if not roots:
604 605 raise util.Abort(_('no matching revisions'))
605 606 if len(roots) > 1:
606 607 raise util.Abort(_("can't rebase multiple roots"))
607 608 root = roots[0]
608 609
609 610 commonbase = root.ancestor(dest)
610 611 if commonbase == root:
611 612 raise util.Abort(_('source is ancestor of destination'))
612 613 if commonbase == dest:
613 614 samebranch = root.branch() == dest.branch()
614 615 if not collapse and samebranch and root in dest.children():
615 616 repo.ui.debug('source is a child of destination\n')
616 617 return None
617 618
618 619 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
619 620 state = dict.fromkeys(rebaseset, nullrev)
620 621 # Rebase tries to turn <dest> into a parent of <root> while
621 622 # preserving the number of parents of rebased changesets:
622 623 #
623 624 # - A changeset with a single parent will always be rebased as a
624 625 # changeset with a single parent.
625 626 #
626 627 # - A merge will be rebased as merge unless its parents are both
627 628 # ancestors of <dest> or are themselves in the rebased set and
628 629 # pruned while rebased.
629 630 #
630 631 # If one parent of <root> is an ancestor of <dest>, the rebased
631 632 # version of this parent will be <dest>. This is always true with
632 633 # --base option.
633 634 #
634 635 # Otherwise, we need to *replace* the original parents with
635 636 # <dest>. This "detaches" the rebased set from its former location
636 637 # and rebases it onto <dest>. Changes introduced by ancestors of
637 638 # <root> not common with <dest> (the detachset, marked as
638 639 # nullmerge) are "removed" from the rebased changesets.
639 640 #
640 641 # - If <root> has a single parent, set it to <dest>.
641 642 #
642 643 # - If <root> is a merge, we cannot decide which parent to
643 644 # replace, the rebase operation is not clearly defined.
644 645 #
645 646 # The table below sums up this behavior:
646 647 #
647 648 # +--------------------+----------------------+-------------------------+
648 649 # | | one parent | merge |
649 650 # +--------------------+----------------------+-------------------------+
650 651 # | parent in ::<dest> | new parent is <dest> | parents in ::<dest> are |
651 652 # | | | remapped to <dest> |
652 653 # +--------------------+----------------------+-------------------------+
653 654 # | unrelated source | new parent is <dest> | ambiguous, abort |
654 655 # +--------------------+----------------------+-------------------------+
655 656 #
656 657 # The actual abort is handled by `defineparents`
657 658 if len(root.parents()) <= 1:
658 659 # ancestors of <root> not ancestors of <dest>
659 660 detachset = repo.changelog.findmissingrevs([commonbase.rev()],
660 661 [root.rev()])
661 662 state.update(dict.fromkeys(detachset, nullmerge))
662 663 # detachset can have root, and we definitely want to rebase that
663 664 state[root.rev()] = nullrev
664 665 return repo['.'].rev(), dest.rev(), state
665 666
666 667 def clearrebased(ui, repo, state, collapsedas=None):
667 668 """dispose of rebased revision at the end of the rebase
668 669
669 670 If `collapsedas` is not None, the rebase was a collapse whose result if the
670 671 `collapsedas` node."""
671 672 if obsolete._enabled:
672 673 markers = []
673 674 for rev, newrev in sorted(state.items()):
674 675 if newrev >= 0:
675 676 if collapsedas is not None:
676 677 newrev = collapsedas
677 678 markers.append((repo[rev], (repo[newrev],)))
678 679 if markers:
679 680 obsolete.createmarkers(repo, markers)
680 681 else:
681 682 rebased = [rev for rev in state if state[rev] != nullmerge]
682 683 if rebased:
683 684 if set(repo.changelog.descendants([min(rebased)])) - set(state):
684 685 ui.warn(_("warning: new changesets detected "
685 686 "on source branch, not stripping\n"))
686 687 else:
687 688 # backup the old csets by default
688 689 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
689 690
690 691
691 692 def pullrebase(orig, ui, repo, *args, **opts):
692 693 'Call rebase after pull if the latter has been invoked with --rebase'
693 694 if opts.get('rebase'):
694 695 if opts.get('update'):
695 696 del opts['update']
696 697 ui.debug('--update and --rebase are not compatible, ignoring '
697 698 'the update flag\n')
698 699
699 700 movemarkfrom = repo['.'].node()
700 701 cmdutil.bailifchanged(repo)
701 702 revsprepull = len(repo)
702 703 origpostincoming = commands.postincoming
703 704 def _dummy(*args, **kwargs):
704 705 pass
705 706 commands.postincoming = _dummy
706 707 try:
707 708 orig(ui, repo, *args, **opts)
708 709 finally:
709 710 commands.postincoming = origpostincoming
710 711 revspostpull = len(repo)
711 712 if revspostpull > revsprepull:
712 713 # --rev option from pull conflict with rebase own --rev
713 714 # dropping it
714 715 if 'rev' in opts:
715 716 del opts['rev']
716 717 rebase(ui, repo, **opts)
717 718 branch = repo[None].branch()
718 719 dest = repo[branch].rev()
719 720 if dest != repo['.'].rev():
720 721 # there was nothing to rebase we force an update
721 722 hg.update(repo, dest)
722 723 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
723 724 ui.status(_("updating bookmark %s\n")
724 725 % repo._bookmarkcurrent)
725 726 else:
726 727 if opts.get('tool'):
727 728 raise util.Abort(_('--tool can only be used with --rebase'))
728 729 orig(ui, repo, *args, **opts)
729 730
730 731 def uisetup(ui):
731 732 'Replace pull with a decorator to provide --rebase option'
732 733 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
733 734 entry[1].append(('', 'rebase', None,
734 735 _("rebase working directory to branch head")))
735 736 entry[1].append(('t', 'tool', '',
736 737 _("specify merge tool for rebase")))
@@ -1,282 +1,308 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 169 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 08483444fef91d6224f6655ee586a65d263ad34c 0 {'date': '*', 'user': 'test'} (glob)
170 170 5fddd98957c8a54a4d436dfe1da9d87f21a1b97b 5ae4c968c6aca831df823664e706c9d4aa34473d 0 {'date': '*', 'user': 'test'} (glob)
171 171 32af7686d403cf45b5d95f2d70cebea587ac806a 5ae4c968c6aca831df823664e706c9d4aa34473d 0 {'date': '*', 'user': 'test'} (glob)
172 172
173 173
174 174 $ cd ..
175 175
176 176 collapse rebase
177 177 ---------------------------------
178 178
179 179 $ hg clone base collapse
180 180 updating to branch default
181 181 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
182 182 $ cd collapse
183 183 $ hg rebase -s 42ccdea3bb16 -d eea13746799a --collapse
184 184 $ hg log -G
185 185 @ 8:4dc2197e807b Collapsed revision
186 186 |
187 187 | o 7:02de42196ebe H
188 188 | |
189 189 o | 6:eea13746799a G
190 190 |\|
191 191 | o 5:24b6387c8c8c F
192 192 | |
193 193 o | 4:9520eea781bc E
194 194 |/
195 195 o 0:cd010b8cd998 A
196 196
197 197 $ hg log --hidden -G
198 198 @ 8:4dc2197e807b Collapsed revision
199 199 |
200 200 | o 7:02de42196ebe H
201 201 | |
202 202 o | 6:eea13746799a G
203 203 |\|
204 204 | o 5:24b6387c8c8c F
205 205 | |
206 206 o | 4:9520eea781bc E
207 207 |/
208 208 | x 3:32af7686d403 D
209 209 | |
210 210 | x 2:5fddd98957c8 C
211 211 | |
212 212 | x 1:42ccdea3bb16 B
213 213 |/
214 214 o 0:cd010b8cd998 A
215 215
216 216 $ hg id --debug
217 217 4dc2197e807bae9817f09905b50ab288be2dbbcf tip
218 218 $ hg debugobsolete
219 219 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 4dc2197e807bae9817f09905b50ab288be2dbbcf 0 {'date': '*', 'user': 'test'} (glob)
220 220 5fddd98957c8a54a4d436dfe1da9d87f21a1b97b 4dc2197e807bae9817f09905b50ab288be2dbbcf 0 {'date': '*', 'user': 'test'} (glob)
221 221 32af7686d403cf45b5d95f2d70cebea587ac806a 4dc2197e807bae9817f09905b50ab288be2dbbcf 0 {'date': '*', 'user': 'test'} (glob)
222 222
223 223 $ cd ..
224 224
225 225 Rebase set has hidden descendants
226 226 ---------------------------------
227 227
228 228 We rebase a changeset which has a hidden changeset. The hidden changeset must
229 229 not be rebased.
230 230
231 231 $ hg clone base hidden
232 232 updating to branch default
233 233 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
234 234 $ cd hidden
235 235 $ hg rebase -s 5fddd98957c8 -d eea13746799a
236 236 $ hg rebase -s 42ccdea3bb16 -d 02de42196ebe
237 237 $ hg log -G
238 238 @ 10:7c6027df6a99 B
239 239 |
240 240 | o 9:cf44d2f5a9f4 D
241 241 | |
242 242 | o 8:e273c5e7d2d2 C
243 243 | |
244 244 o | 7:02de42196ebe H
245 245 | |
246 246 | o 6:eea13746799a G
247 247 |/|
248 248 o | 5:24b6387c8c8c F
249 249 | |
250 250 | o 4:9520eea781bc E
251 251 |/
252 252 o 0:cd010b8cd998 A
253 253
254 254 $ hg log --hidden -G
255 255 @ 10:7c6027df6a99 B
256 256 |
257 257 | o 9:cf44d2f5a9f4 D
258 258 | |
259 259 | o 8:e273c5e7d2d2 C
260 260 | |
261 261 o | 7:02de42196ebe H
262 262 | |
263 263 | o 6:eea13746799a G
264 264 |/|
265 265 o | 5:24b6387c8c8c F
266 266 | |
267 267 | o 4:9520eea781bc E
268 268 |/
269 269 | x 3:32af7686d403 D
270 270 | |
271 271 | x 2:5fddd98957c8 C
272 272 | |
273 273 | x 1:42ccdea3bb16 B
274 274 |/
275 275 o 0:cd010b8cd998 A
276 276
277 277 $ hg debugobsolete
278 278 5fddd98957c8a54a4d436dfe1da9d87f21a1b97b e273c5e7d2d29df783dce9f9eaa3ac4adc69c15d 0 {'date': '*', 'user': 'test'} (glob)
279 279 32af7686d403cf45b5d95f2d70cebea587ac806a cf44d2f5a9f4297a62be94cbdd3dff7c7dc54258 0 {'date': '*', 'user': 'test'} (glob)
280 280 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 7c6027df6a99d93f461868e5433f63bde20b6dfb 0 {'date': '*', 'user': 'test'} (glob)
281 281
282 $ cd ..
282 Test ui.prevent-unstable option
283 ------------------------------------
284
285 $ hg log -r 'children(8)'
286 9:cf44d2f5a9f4 D (no-eol)
287 $ hg rebase -r 8
288 $ hg log -G
289 @ 11:0d8f238b634c C
290 |
291 o 10:7c6027df6a99 B
292 |
293 | o 9:cf44d2f5a9f4 D
294 | |
295 | x 8:e273c5e7d2d2 C
296 | |
297 o | 7:02de42196ebe H
298 | |
299 | o 6:eea13746799a G
300 |/|
301 o | 5:24b6387c8c8c F
302 | |
303 | o 4:9520eea781bc E
304 |/
305 o 0:cd010b8cd998 A
306
307
308
General Comments 0
You need to be logged in to leave comments. Login now