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