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