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