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