##// END OF EJS Templates
rebase: fix selection of base used when rebasing merge (issue4041)...
Pierre-Yves David -
r19969:ad9db007 stable
parent child Browse files
Show More
1 NO CONTENT: new file 100644, binary diff hidden
@@ -1,875 +1,910 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, error
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 def _savegraft(ctx, extra):
33 33 s = ctx.extra().get('source', None)
34 34 if s is not None:
35 35 extra['source'] = s
36 36
37 37 def _savebranch(ctx, extra):
38 38 extra['branch'] = ctx.branch()
39 39
40 40 def _makeextrafn(copiers):
41 41 """make an extrafn out of the given copy-functions.
42 42
43 43 A copy function takes a context and an extra dict, and mutates the
44 44 extra dict as needed based on the given context.
45 45 """
46 46 def extrafn(ctx, extra):
47 47 for c in copiers:
48 48 c(ctx, extra)
49 49 return extrafn
50 50
51 51 @command('rebase',
52 52 [('s', 'source', '',
53 53 _('rebase from the specified changeset'), _('REV')),
54 54 ('b', 'base', '',
55 55 _('rebase from the base of the specified changeset '
56 56 '(up to greatest common ancestor of base and dest)'),
57 57 _('REV')),
58 58 ('r', 'rev', [],
59 59 _('rebase these revisions'),
60 60 _('REV')),
61 61 ('d', 'dest', '',
62 62 _('rebase onto the specified changeset'), _('REV')),
63 63 ('', 'collapse', False, _('collapse the rebased changesets')),
64 64 ('m', 'message', '',
65 65 _('use text as collapse commit message'), _('TEXT')),
66 66 ('e', 'edit', False, _('invoke editor on commit messages')),
67 67 ('l', 'logfile', '',
68 68 _('read collapse commit message from file'), _('FILE')),
69 69 ('', 'keep', False, _('keep original changesets')),
70 70 ('', 'keepbranches', False, _('keep original branch names')),
71 71 ('D', 'detach', False, _('(DEPRECATED)')),
72 72 ('t', 'tool', '', _('specify merge tool')),
73 73 ('c', 'continue', False, _('continue an interrupted rebase')),
74 74 ('a', 'abort', False, _('abort an interrupted rebase'))] +
75 75 templateopts,
76 76 _('[-s REV | -b REV] [-d REV] [OPTION]'))
77 77 def rebase(ui, repo, **opts):
78 78 """move changeset (and descendants) to a different branch
79 79
80 80 Rebase uses repeated merging to graft changesets from one part of
81 81 history (the source) onto another (the destination). This can be
82 82 useful for linearizing *local* changes relative to a master
83 83 development tree.
84 84
85 85 You should not rebase changesets that have already been shared
86 86 with others. Doing so will force everybody else to perform the
87 87 same rebase or they will end up with duplicated changesets after
88 88 pulling in your rebased changesets.
89 89
90 90 In its default configuration, Mercurial will prevent you from
91 91 rebasing published changes. See :hg:`help phases` for details.
92 92
93 93 If you don't specify a destination changeset (``-d/--dest``),
94 94 rebase uses the current branch tip as the destination. (The
95 95 destination changeset is not modified by rebasing, but new
96 96 changesets are added as its descendants.)
97 97
98 98 You can specify which changesets to rebase in two ways: as a
99 99 "source" changeset or as a "base" changeset. Both are shorthand
100 100 for a topologically related set of changesets (the "source
101 101 branch"). If you specify source (``-s/--source``), rebase will
102 102 rebase that changeset and all of its descendants onto dest. If you
103 103 specify base (``-b/--base``), rebase will select ancestors of base
104 104 back to but not including the common ancestor with dest. Thus,
105 105 ``-b`` is less precise but more convenient than ``-s``: you can
106 106 specify any changeset in the source branch, and rebase will select
107 107 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
108 108 uses the parent of the working directory as the base.
109 109
110 110 For advanced usage, a third way is available through the ``--rev``
111 111 option. It allows you to specify an arbitrary set of changesets to
112 112 rebase. Descendants of revs you specify with this option are not
113 113 automatically included in the rebase.
114 114
115 115 By default, rebase recreates the changesets in the source branch
116 116 as descendants of dest and then destroys the originals. Use
117 117 ``--keep`` to preserve the original source changesets. Some
118 118 changesets in the source branch (e.g. merges from the destination
119 119 branch) may be dropped if they no longer contribute any change.
120 120
121 121 One result of the rules for selecting the destination changeset
122 122 and source branch is that, unlike ``merge``, rebase will do
123 123 nothing if you are at the branch tip of a named branch
124 124 with two heads. You need to explicitly specify source and/or
125 125 destination (or ``update`` to the other head, if it's the head of
126 126 the intended source branch).
127 127
128 128 If a rebase is interrupted to manually resolve a merge, it can be
129 129 continued with --continue/-c or aborted with --abort/-a.
130 130
131 131 Returns 0 on success, 1 if nothing to rebase.
132 132 """
133 133 originalwd = target = None
134 134 activebookmark = None
135 135 external = nullrev
136 136 state = {}
137 137 skipped = set()
138 138 targetancestors = set()
139 139
140 140 editor = None
141 141 if opts.get('edit'):
142 142 editor = cmdutil.commitforceeditor
143 143
144 144 lock = wlock = None
145 145 try:
146 146 wlock = repo.wlock()
147 147 lock = repo.lock()
148 148
149 149 # Validate input and define rebasing points
150 150 destf = opts.get('dest', None)
151 151 srcf = opts.get('source', None)
152 152 basef = opts.get('base', None)
153 153 revf = opts.get('rev', [])
154 154 contf = opts.get('continue')
155 155 abortf = opts.get('abort')
156 156 collapsef = opts.get('collapse', False)
157 157 collapsemsg = cmdutil.logmessage(ui, opts)
158 158 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
159 159 extrafns = [_savegraft]
160 160 if e:
161 161 extrafns = [e]
162 162 keepf = opts.get('keep', False)
163 163 keepbranchesf = opts.get('keepbranches', False)
164 164 # keepopen is not meant for use on the command line, but by
165 165 # other extensions
166 166 keepopen = opts.get('keepopen', False)
167 167
168 168 if collapsemsg and not collapsef:
169 169 raise util.Abort(
170 170 _('message can only be specified with collapse'))
171 171
172 172 if contf or abortf:
173 173 if contf and abortf:
174 174 raise util.Abort(_('cannot use both abort and continue'))
175 175 if collapsef:
176 176 raise util.Abort(
177 177 _('cannot use collapse with continue or abort'))
178 178 if srcf or basef or destf:
179 179 raise util.Abort(
180 180 _('abort and continue do not allow specifying revisions'))
181 181 if opts.get('tool', False):
182 182 ui.warn(_('tool option will be ignored\n'))
183 183
184 184 try:
185 185 (originalwd, target, state, skipped, collapsef, keepf,
186 186 keepbranchesf, external, activebookmark) = restorestatus(repo)
187 187 except error.RepoLookupError:
188 188 if abortf:
189 189 clearstatus(repo)
190 190 repo.ui.warn(_('rebase aborted (no revision is removed,'
191 191 ' only broken state is cleared)\n'))
192 192 return 0
193 193 else:
194 194 msg = _('cannot continue inconsistent rebase')
195 195 hint = _('use "hg rebase --abort" to clear borken state')
196 196 raise util.Abort(msg, hint=hint)
197 197 if abortf:
198 198 return abort(repo, originalwd, target, state)
199 199 else:
200 200 if srcf and basef:
201 201 raise util.Abort(_('cannot specify both a '
202 202 'source and a base'))
203 203 if revf and basef:
204 204 raise util.Abort(_('cannot specify both a '
205 205 'revision and a base'))
206 206 if revf and srcf:
207 207 raise util.Abort(_('cannot specify both a '
208 208 'revision and a source'))
209 209
210 210 cmdutil.checkunfinished(repo)
211 211 cmdutil.bailifchanged(repo)
212 212
213 213 if not destf:
214 214 # Destination defaults to the latest revision in the
215 215 # current branch
216 216 branch = repo[None].branch()
217 217 dest = repo[branch]
218 218 else:
219 219 dest = scmutil.revsingle(repo, destf)
220 220
221 221 if revf:
222 222 rebaseset = scmutil.revrange(repo, revf)
223 223 elif srcf:
224 224 src = scmutil.revrange(repo, [srcf])
225 225 rebaseset = repo.revs('(%ld)::', src)
226 226 else:
227 227 base = scmutil.revrange(repo, [basef or '.'])
228 228 rebaseset = repo.revs(
229 229 '(children(ancestor(%ld, %d)) and ::(%ld))::',
230 230 base, dest, base)
231 231 if rebaseset:
232 232 root = min(rebaseset)
233 233 else:
234 234 root = None
235 235
236 236 if not rebaseset:
237 237 repo.ui.debug('base is ancestor of destination\n')
238 238 result = None
239 239 elif (not (keepf or obsolete._enabled)
240 240 and repo.revs('first(children(%ld) - %ld)',
241 241 rebaseset, rebaseset)):
242 242 raise util.Abort(
243 243 _("can't remove original changesets with"
244 244 " unrebased descendants"),
245 245 hint=_('use --keep to keep original changesets'))
246 246 else:
247 247 result = buildstate(repo, dest, rebaseset, collapsef)
248 248
249 249 if not result:
250 250 # Empty state built, nothing to rebase
251 251 ui.status(_('nothing to rebase\n'))
252 252 return 1
253 253 elif not keepf and not repo[root].mutable():
254 254 raise util.Abort(_("can't rebase immutable changeset %s")
255 255 % repo[root],
256 256 hint=_('see hg help phases for details'))
257 257 else:
258 258 originalwd, target, state = result
259 259 if collapsef:
260 260 targetancestors = repo.changelog.ancestors([target],
261 261 inclusive=True)
262 262 external = externalparent(repo, state, targetancestors)
263 263
264 264 if keepbranchesf:
265 265 # insert _savebranch at the start of extrafns so if
266 266 # there's a user-provided extrafn it can clobber branch if
267 267 # desired
268 268 extrafns.insert(0, _savebranch)
269 269 if collapsef:
270 270 branches = set()
271 271 for rev in state:
272 272 branches.add(repo[rev].branch())
273 273 if len(branches) > 1:
274 274 raise util.Abort(_('cannot collapse multiple named '
275 275 'branches'))
276 276
277 277
278 278 # Rebase
279 279 if not targetancestors:
280 280 targetancestors = repo.changelog.ancestors([target], inclusive=True)
281 281
282 282 # Keep track of the current bookmarks in order to reset them later
283 283 currentbookmarks = repo._bookmarks.copy()
284 284 activebookmark = activebookmark or repo._bookmarkcurrent
285 285 if activebookmark:
286 286 bookmarks.unsetcurrent(repo)
287 287
288 288 extrafn = _makeextrafn(extrafns)
289 289
290 290 sortedstate = sorted(state)
291 291 total = len(sortedstate)
292 292 pos = 0
293 293 for rev in sortedstate:
294 294 pos += 1
295 295 if state[rev] == -1:
296 296 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
297 297 _('changesets'), total)
298 298 p1, p2 = defineparents(repo, rev, target, state,
299 299 targetancestors)
300 300 storestatus(repo, originalwd, target, state, collapsef, keepf,
301 301 keepbranchesf, external, activebookmark)
302 302 if len(repo.parents()) == 2:
303 303 repo.ui.debug('resuming interrupted rebase\n')
304 304 else:
305 305 try:
306 306 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
307 307 stats = rebasenode(repo, rev, p1, state, collapsef)
308 308 if stats and stats[3] > 0:
309 309 raise error.InterventionRequired(
310 310 _('unresolved conflicts (see hg '
311 311 'resolve, then hg rebase --continue)'))
312 312 finally:
313 313 ui.setconfig('ui', 'forcemerge', '')
314 314 cmdutil.duplicatecopies(repo, rev, target)
315 315 if not collapsef:
316 316 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
317 317 editor=editor)
318 318 else:
319 319 # Skip commit if we are collapsing
320 320 repo.setparents(repo[p1].node())
321 321 newrev = None
322 322 # Update the state
323 323 if newrev is not None:
324 324 state[rev] = repo[newrev].rev()
325 325 else:
326 326 if not collapsef:
327 327 ui.note(_('no changes, revision %d skipped\n') % rev)
328 328 ui.debug('next revision set to %s\n' % p1)
329 329 skipped.add(rev)
330 330 state[rev] = p1
331 331
332 332 ui.progress(_('rebasing'), None)
333 333 ui.note(_('rebase merging completed\n'))
334 334
335 335 if collapsef and not keepopen:
336 336 p1, p2 = defineparents(repo, min(state), target,
337 337 state, targetancestors)
338 338 if collapsemsg:
339 339 commitmsg = collapsemsg
340 340 else:
341 341 commitmsg = 'Collapsed revision'
342 342 for rebased in state:
343 343 if rebased not in skipped and state[rebased] > nullmerge:
344 344 commitmsg += '\n* %s' % repo[rebased].description()
345 345 commitmsg = ui.edit(commitmsg, repo.ui.username())
346 346 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
347 347 extrafn=extrafn, editor=editor)
348 348
349 349 if 'qtip' in repo.tags():
350 350 updatemq(repo, state, skipped, **opts)
351 351
352 352 if currentbookmarks:
353 353 # Nodeids are needed to reset bookmarks
354 354 nstate = {}
355 355 for k, v in state.iteritems():
356 356 if v > nullmerge:
357 357 nstate[repo[k].node()] = repo[v].node()
358 358 # XXX this is the same as dest.node() for the non-continue path --
359 359 # this should probably be cleaned up
360 360 targetnode = repo[target].node()
361 361
362 362 # restore original working directory
363 363 # (we do this before stripping)
364 364 newwd = state.get(originalwd, originalwd)
365 365 if newwd not in [c.rev() for c in repo[None].parents()]:
366 366 ui.note(_("update back to initial working directory parent\n"))
367 367 hg.updaterepo(repo, newwd, False)
368 368
369 369 if not keepf:
370 370 collapsedas = None
371 371 if collapsef:
372 372 collapsedas = newrev
373 373 clearrebased(ui, repo, state, skipped, collapsedas)
374 374
375 375 if currentbookmarks:
376 376 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
377 377
378 378 clearstatus(repo)
379 379 ui.note(_("rebase completed\n"))
380 380 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
381 381 if skipped:
382 382 ui.note(_("%d revisions have been skipped\n") % len(skipped))
383 383
384 384 if (activebookmark and
385 385 repo['.'].node() == repo._bookmarks[activebookmark]):
386 386 bookmarks.setcurrent(repo, activebookmark)
387 387
388 388 finally:
389 389 release(lock, wlock)
390 390
391 391 def externalparent(repo, state, targetancestors):
392 392 """Return the revision that should be used as the second parent
393 393 when the revisions in state is collapsed on top of targetancestors.
394 394 Abort if there is more than one parent.
395 395 """
396 396 parents = set()
397 397 source = min(state)
398 398 for rev in state:
399 399 if rev == source:
400 400 continue
401 401 for p in repo[rev].parents():
402 402 if (p.rev() not in state
403 403 and p.rev() not in targetancestors):
404 404 parents.add(p.rev())
405 405 if not parents:
406 406 return nullrev
407 407 if len(parents) == 1:
408 408 return parents.pop()
409 409 raise util.Abort(_('unable to collapse on top of %s, there is more '
410 410 'than one external parent: %s') %
411 411 (max(targetancestors),
412 412 ', '.join(str(p) for p in sorted(parents))))
413 413
414 414 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
415 415 'Commit the changes and store useful information in extra'
416 416 try:
417 417 repo.setparents(repo[p1].node(), repo[p2].node())
418 418 ctx = repo[rev]
419 419 if commitmsg is None:
420 420 commitmsg = ctx.description()
421 421 extra = {'rebase_source': ctx.hex()}
422 422 if extrafn:
423 423 extrafn(ctx, extra)
424 424 # Commit might fail if unresolved files exist
425 425 newrev = repo.commit(text=commitmsg, user=ctx.user(),
426 426 date=ctx.date(), extra=extra, editor=editor)
427 427 repo.dirstate.setbranch(repo[newrev].branch())
428 428 targetphase = max(ctx.phase(), phases.draft)
429 429 # retractboundary doesn't overwrite upper phase inherited from parent
430 430 newnode = repo[newrev].node()
431 431 if newnode:
432 432 phases.retractboundary(repo, targetphase, [newnode])
433 433 return newrev
434 434 except util.Abort:
435 435 # Invalidate the previous setparents
436 436 repo.dirstate.invalidate()
437 437 raise
438 438
439 439 def rebasenode(repo, rev, p1, state, collapse):
440 440 'Rebase a single revision'
441 441 # Merge phase
442 442 # Update to target and merge it with local
443 443 if repo['.'].rev() != repo[p1].rev():
444 444 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
445 445 merge.update(repo, p1, False, True, False)
446 446 else:
447 447 repo.ui.debug(" already in target\n")
448 448 repo.dirstate.write()
449 449 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
450 base = None
451 if repo[rev].rev() != repo[min(state)].rev():
450 if repo[rev].rev() == repo[min(state)].rev():
451 # Case (1) initial changeset of a non-detaching rebase.
452 # Let the merge mechanism find the base itself.
453 base = None
454 elif not repo[rev].p2():
455 # Case (2) detaching the node with a single parent, use this parent
452 456 base = repo[rev].p1().node()
457 else:
458 # In case of merge, we need to pick the right parent as merge base.
459 #
460 # Imagine we have:
461 # - M: currently rebase revision in this step
462 # - A: one parent of M
463 # - B: second parent of M
464 # - D: destination of this merge step (p1 var)
465 #
466 # If we are rebasing on D, D is the successors of A or B. The right
467 # merge base is the one D succeed to. We pretend it is B for the rest
468 # of this comment
469 #
470 # If we pick B as the base, the merge involves:
471 # - changes from B to M (actual changeset payload)
472 # - changes from B to D (induced by rebase) as D is a rebased
473 # version of B)
474 # Which exactly represent the rebase operation.
475 #
476 # If we pick the A as the base, the merge involves
477 # - changes from A to M (actual changeset payload)
478 # - changes from A to D (with include changes between unrelated A and B
479 # plus changes induced by rebase)
480 # Which does not represent anything sensible and creates a lot of
481 # conflicts.
482 for p in repo[rev].parents():
483 if state.get(p.rev()) == repo[p1].rev():
484 base = p.node()
485 break
486 if base is not None:
487 repo.ui.debug(" detach base %d:%s\n" % (repo[base].rev(), repo[base]))
453 488 # When collapsing in-place, the parent is the common ancestor, we
454 489 # have to allow merging with it.
455 490 return merge.update(repo, rev, True, True, False, base, collapse)
456 491
457 492 def nearestrebased(repo, rev, state):
458 493 """return the nearest ancestors of rev in the rebase result"""
459 494 rebased = [r for r in state if state[r] > nullmerge]
460 495 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
461 496 if candidates:
462 497 return state[candidates[0]]
463 498 else:
464 499 return None
465 500
466 501 def defineparents(repo, rev, target, state, targetancestors):
467 502 'Return the new parent relationship of the revision that will be rebased'
468 503 parents = repo[rev].parents()
469 504 p1 = p2 = nullrev
470 505
471 506 P1n = parents[0].rev()
472 507 if P1n in targetancestors:
473 508 p1 = target
474 509 elif P1n in state:
475 510 if state[P1n] == nullmerge:
476 511 p1 = target
477 512 elif state[P1n] == revignored:
478 513 p1 = nearestrebased(repo, P1n, state)
479 514 if p1 is None:
480 515 p1 = target
481 516 else:
482 517 p1 = state[P1n]
483 518 else: # P1n external
484 519 p1 = target
485 520 p2 = P1n
486 521
487 522 if len(parents) == 2 and parents[1].rev() not in targetancestors:
488 523 P2n = parents[1].rev()
489 524 # interesting second parent
490 525 if P2n in state:
491 526 if p1 == target: # P1n in targetancestors or external
492 527 p1 = state[P2n]
493 528 elif state[P2n] == revignored:
494 529 p2 = nearestrebased(repo, P2n, state)
495 530 if p2 is None:
496 531 # no ancestors rebased yet, detach
497 532 p2 = target
498 533 else:
499 534 p2 = state[P2n]
500 535 else: # P2n external
501 536 if p2 != nullrev: # P1n external too => rev is a merged revision
502 537 raise util.Abort(_('cannot use revision %d as base, result '
503 538 'would have 3 parents') % rev)
504 539 p2 = P2n
505 540 repo.ui.debug(" future parents are %d and %d\n" %
506 541 (repo[p1].rev(), repo[p2].rev()))
507 542 return p1, p2
508 543
509 544 def isagitpatch(repo, patchname):
510 545 'Return true if the given patch is in git format'
511 546 mqpatch = os.path.join(repo.mq.path, patchname)
512 547 for line in patch.linereader(file(mqpatch, 'rb')):
513 548 if line.startswith('diff --git'):
514 549 return True
515 550 return False
516 551
517 552 def updatemq(repo, state, skipped, **opts):
518 553 'Update rebased mq patches - finalize and then import them'
519 554 mqrebase = {}
520 555 mq = repo.mq
521 556 original_series = mq.fullseries[:]
522 557 skippedpatches = set()
523 558
524 559 for p in mq.applied:
525 560 rev = repo[p.node].rev()
526 561 if rev in state:
527 562 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
528 563 (rev, p.name))
529 564 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
530 565 else:
531 566 # Applied but not rebased, not sure this should happen
532 567 skippedpatches.add(p.name)
533 568
534 569 if mqrebase:
535 570 mq.finish(repo, mqrebase.keys())
536 571
537 572 # We must start import from the newest revision
538 573 for rev in sorted(mqrebase, reverse=True):
539 574 if rev not in skipped:
540 575 name, isgit = mqrebase[rev]
541 576 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
542 577 mq.qimport(repo, (), patchname=name, git=isgit,
543 578 rev=[str(state[rev])])
544 579 else:
545 580 # Rebased and skipped
546 581 skippedpatches.add(mqrebase[rev][0])
547 582
548 583 # Patches were either applied and rebased and imported in
549 584 # order, applied and removed or unapplied. Discard the removed
550 585 # ones while preserving the original series order and guards.
551 586 newseries = [s for s in original_series
552 587 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
553 588 mq.fullseries[:] = newseries
554 589 mq.seriesdirty = True
555 590 mq.savedirty()
556 591
557 592 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
558 593 'Move bookmarks to their correct changesets, and delete divergent ones'
559 594 marks = repo._bookmarks
560 595 for k, v in originalbookmarks.iteritems():
561 596 if v in nstate:
562 597 # update the bookmarks for revs that have moved
563 598 marks[k] = nstate[v]
564 599 bookmarks.deletedivergent(repo, [targetnode], k)
565 600
566 601 marks.write()
567 602
568 603 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
569 604 external, activebookmark):
570 605 'Store the current status to allow recovery'
571 606 f = repo.opener("rebasestate", "w")
572 607 f.write(repo[originalwd].hex() + '\n')
573 608 f.write(repo[target].hex() + '\n')
574 609 f.write(repo[external].hex() + '\n')
575 610 f.write('%d\n' % int(collapse))
576 611 f.write('%d\n' % int(keep))
577 612 f.write('%d\n' % int(keepbranches))
578 613 f.write('%s\n' % (activebookmark or ''))
579 614 for d, v in state.iteritems():
580 615 oldrev = repo[d].hex()
581 616 if v > nullmerge:
582 617 newrev = repo[v].hex()
583 618 else:
584 619 newrev = v
585 620 f.write("%s:%s\n" % (oldrev, newrev))
586 621 f.close()
587 622 repo.ui.debug('rebase status stored\n')
588 623
589 624 def clearstatus(repo):
590 625 'Remove the status files'
591 626 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
592 627
593 628 def restorestatus(repo):
594 629 'Restore a previously stored status'
595 630 try:
596 631 target = None
597 632 collapse = False
598 633 external = nullrev
599 634 activebookmark = None
600 635 state = {}
601 636 f = repo.opener("rebasestate")
602 637 for i, l in enumerate(f.read().splitlines()):
603 638 if i == 0:
604 639 originalwd = repo[l].rev()
605 640 elif i == 1:
606 641 target = repo[l].rev()
607 642 elif i == 2:
608 643 external = repo[l].rev()
609 644 elif i == 3:
610 645 collapse = bool(int(l))
611 646 elif i == 4:
612 647 keep = bool(int(l))
613 648 elif i == 5:
614 649 keepbranches = bool(int(l))
615 650 elif i == 6 and not (len(l) == 81 and ':' in l):
616 651 # line 6 is a recent addition, so for backwards compatibility
617 652 # check that the line doesn't look like the oldrev:newrev lines
618 653 activebookmark = l
619 654 else:
620 655 oldrev, newrev = l.split(':')
621 656 if newrev in (str(nullmerge), str(revignored)):
622 657 state[repo[oldrev].rev()] = int(newrev)
623 658 else:
624 659 state[repo[oldrev].rev()] = repo[newrev].rev()
625 660 skipped = set()
626 661 # recompute the set of skipped revs
627 662 if not collapse:
628 663 seen = set([target])
629 664 for old, new in sorted(state.items()):
630 665 if new != nullrev and new in seen:
631 666 skipped.add(old)
632 667 seen.add(new)
633 668 repo.ui.debug('computed skipped revs: %s\n' % skipped)
634 669 repo.ui.debug('rebase status resumed\n')
635 670 return (originalwd, target, state, skipped,
636 671 collapse, keep, keepbranches, external, activebookmark)
637 672 except IOError, err:
638 673 if err.errno != errno.ENOENT:
639 674 raise
640 675 raise util.Abort(_('no rebase in progress'))
641 676
642 677 def inrebase(repo, originalwd, state):
643 678 '''check whether the working dir is in an interrupted rebase'''
644 679 parents = [p.rev() for p in repo.parents()]
645 680 if originalwd in parents:
646 681 return True
647 682
648 683 for newrev in state.itervalues():
649 684 if newrev in parents:
650 685 return True
651 686
652 687 return False
653 688
654 689 def abort(repo, originalwd, target, state):
655 690 'Restore the repository to its original state'
656 691 dstates = [s for s in state.values() if s != nullrev]
657 692 immutable = [d for d in dstates if not repo[d].mutable()]
658 693 cleanup = True
659 694 if immutable:
660 695 repo.ui.warn(_("warning: can't clean up immutable changesets %s\n")
661 696 % ', '.join(str(repo[r]) for r in immutable),
662 697 hint=_('see hg help phases for details'))
663 698 cleanup = False
664 699
665 700 descendants = set()
666 701 if dstates:
667 702 descendants = set(repo.changelog.descendants(dstates))
668 703 if descendants - set(dstates):
669 704 repo.ui.warn(_("warning: new changesets detected on target branch, "
670 705 "can't strip\n"))
671 706 cleanup = False
672 707
673 708 if cleanup:
674 709 # Update away from the rebase if necessary
675 710 if inrebase(repo, originalwd, state):
676 711 merge.update(repo, repo[originalwd].rev(), False, True, False)
677 712
678 713 # Strip from the first rebased revision
679 714 rebased = filter(lambda x: x > -1 and x != target, state.values())
680 715 if rebased:
681 716 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
682 717 # no backup of rebased cset versions needed
683 718 repair.strip(repo.ui, repo, strippoints)
684 719
685 720 clearstatus(repo)
686 721 repo.ui.warn(_('rebase aborted\n'))
687 722 return 0
688 723
689 724 def buildstate(repo, dest, rebaseset, collapse):
690 725 '''Define which revisions are going to be rebased and where
691 726
692 727 repo: repo
693 728 dest: context
694 729 rebaseset: set of rev
695 730 '''
696 731
697 732 # This check isn't strictly necessary, since mq detects commits over an
698 733 # applied patch. But it prevents messing up the working directory when
699 734 # a partially completed rebase is blocked by mq.
700 735 if 'qtip' in repo.tags() and (dest.node() in
701 736 [s.node for s in repo.mq.applied]):
702 737 raise util.Abort(_('cannot rebase onto an applied mq patch'))
703 738
704 739 roots = list(repo.set('roots(%ld)', rebaseset))
705 740 if not roots:
706 741 raise util.Abort(_('no matching revisions'))
707 742 roots.sort()
708 743 state = {}
709 744 detachset = set()
710 745 for root in roots:
711 746 commonbase = root.ancestor(dest)
712 747 if commonbase == root:
713 748 raise util.Abort(_('source is ancestor of destination'))
714 749 if commonbase == dest:
715 750 samebranch = root.branch() == dest.branch()
716 751 if not collapse and samebranch and root in dest.children():
717 752 repo.ui.debug('source is a child of destination\n')
718 753 return None
719 754
720 755 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))
721 756 state.update(dict.fromkeys(rebaseset, nullrev))
722 757 # Rebase tries to turn <dest> into a parent of <root> while
723 758 # preserving the number of parents of rebased changesets:
724 759 #
725 760 # - A changeset with a single parent will always be rebased as a
726 761 # changeset with a single parent.
727 762 #
728 763 # - A merge will be rebased as merge unless its parents are both
729 764 # ancestors of <dest> or are themselves in the rebased set and
730 765 # pruned while rebased.
731 766 #
732 767 # If one parent of <root> is an ancestor of <dest>, the rebased
733 768 # version of this parent will be <dest>. This is always true with
734 769 # --base option.
735 770 #
736 771 # Otherwise, we need to *replace* the original parents with
737 772 # <dest>. This "detaches" the rebased set from its former location
738 773 # and rebases it onto <dest>. Changes introduced by ancestors of
739 774 # <root> not common with <dest> (the detachset, marked as
740 775 # nullmerge) are "removed" from the rebased changesets.
741 776 #
742 777 # - If <root> has a single parent, set it to <dest>.
743 778 #
744 779 # - If <root> is a merge, we cannot decide which parent to
745 780 # replace, the rebase operation is not clearly defined.
746 781 #
747 782 # The table below sums up this behavior:
748 783 #
749 784 # +------------------+----------------------+-------------------------+
750 785 # | | one parent | merge |
751 786 # +------------------+----------------------+-------------------------+
752 787 # | parent in | new parent is <dest> | parents in ::<dest> are |
753 788 # | ::<dest> | | remapped to <dest> |
754 789 # +------------------+----------------------+-------------------------+
755 790 # | unrelated source | new parent is <dest> | ambiguous, abort |
756 791 # +------------------+----------------------+-------------------------+
757 792 #
758 793 # The actual abort is handled by `defineparents`
759 794 if len(root.parents()) <= 1:
760 795 # ancestors of <root> not ancestors of <dest>
761 796 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
762 797 [root.rev()]))
763 798 for r in detachset:
764 799 if r not in state:
765 800 state[r] = nullmerge
766 801 if len(roots) > 1:
767 802 # If we have multiple roots, we may have "hole" in the rebase set.
768 803 # Rebase roots that descend from those "hole" should not be detached as
769 804 # other root are. We use the special `revignored` to inform rebase that
770 805 # the revision should be ignored but that `defineparents` should search
771 806 # a rebase destination that make sense regarding rebased topology.
772 807 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
773 808 for ignored in set(rebasedomain) - set(rebaseset):
774 809 state[ignored] = revignored
775 810 return repo['.'].rev(), dest.rev(), state
776 811
777 812 def clearrebased(ui, repo, state, skipped, collapsedas=None):
778 813 """dispose of rebased revision at the end of the rebase
779 814
780 815 If `collapsedas` is not None, the rebase was a collapse whose result if the
781 816 `collapsedas` node."""
782 817 if obsolete._enabled:
783 818 markers = []
784 819 for rev, newrev in sorted(state.items()):
785 820 if newrev >= 0:
786 821 if rev in skipped:
787 822 succs = ()
788 823 elif collapsedas is not None:
789 824 succs = (repo[collapsedas],)
790 825 else:
791 826 succs = (repo[newrev],)
792 827 markers.append((repo[rev], succs))
793 828 if markers:
794 829 obsolete.createmarkers(repo, markers)
795 830 else:
796 831 rebased = [rev for rev in state if state[rev] > nullmerge]
797 832 if rebased:
798 833 stripped = []
799 834 for root in repo.set('roots(%ld)', rebased):
800 835 if set(repo.changelog.descendants([root.rev()])) - set(state):
801 836 ui.warn(_("warning: new changesets detected "
802 837 "on source branch, not stripping\n"))
803 838 else:
804 839 stripped.append(root.node())
805 840 if stripped:
806 841 # backup the old csets by default
807 842 repair.strip(ui, repo, stripped, "all")
808 843
809 844
810 845 def pullrebase(orig, ui, repo, *args, **opts):
811 846 'Call rebase after pull if the latter has been invoked with --rebase'
812 847 if opts.get('rebase'):
813 848 if opts.get('update'):
814 849 del opts['update']
815 850 ui.debug('--update and --rebase are not compatible, ignoring '
816 851 'the update flag\n')
817 852
818 853 movemarkfrom = repo['.'].node()
819 854 revsprepull = len(repo)
820 855 origpostincoming = commands.postincoming
821 856 def _dummy(*args, **kwargs):
822 857 pass
823 858 commands.postincoming = _dummy
824 859 try:
825 860 orig(ui, repo, *args, **opts)
826 861 finally:
827 862 commands.postincoming = origpostincoming
828 863 revspostpull = len(repo)
829 864 if revspostpull > revsprepull:
830 865 # --rev option from pull conflict with rebase own --rev
831 866 # dropping it
832 867 if 'rev' in opts:
833 868 del opts['rev']
834 869 rebase(ui, repo, **opts)
835 870 branch = repo[None].branch()
836 871 dest = repo[branch].rev()
837 872 if dest != repo['.'].rev():
838 873 # there was nothing to rebase we force an update
839 874 hg.update(repo, dest)
840 875 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
841 876 ui.status(_("updating bookmark %s\n")
842 877 % repo._bookmarkcurrent)
843 878 else:
844 879 if opts.get('tool'):
845 880 raise util.Abort(_('--tool can only be used with --rebase'))
846 881 orig(ui, repo, *args, **opts)
847 882
848 883 def summaryhook(ui, repo):
849 884 if not os.path.exists(repo.join('rebasestate')):
850 885 return
851 886 try:
852 887 state = restorestatus(repo)[2]
853 888 except error.RepoLookupError:
854 889 # i18n: column positioning for "hg summary"
855 890 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
856 891 ui.write(msg)
857 892 return
858 893 numrebased = len([i for i in state.itervalues() if i != -1])
859 894 # i18n: column positioning for "hg summary"
860 895 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
861 896 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
862 897 ui.label(_('%d remaining'), 'rebase.remaining') %
863 898 (len(state) - numrebased)))
864 899
865 900 def uisetup(ui):
866 901 'Replace pull with a decorator to provide --rebase option'
867 902 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
868 903 entry[1].append(('', 'rebase', None,
869 904 _("rebase working directory to branch head")))
870 905 entry[1].append(('t', 'tool', '',
871 906 _("specify merge tool for rebase")))
872 907 cmdutil.summaryhooks.add('rebase', summaryhook)
873 908 cmdutil.unfinishedstates.append(
874 909 ['rebasestate', False, False, _('rebase in progress'),
875 910 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
@@ -1,126 +1,307 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}:{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 39 $ hg bookmark mybook
40 40
41 41 $ hg phase --force --secret 4
42 42
43 43 $ hg tglog
44 44 @ 5:secret 'L3' mybook
45 45 |
46 46 o 4:secret 'L2'
47 47 |
48 48 o 3:draft 'L1'
49 49 |
50 50 | o 2:draft 'C3'
51 51 |/
52 52 o 1:draft 'C2'
53 53 |
54 54 o 0:draft 'C1'
55 55
56 56 Try to call --continue:
57 57
58 58 $ hg rebase --continue
59 59 abort: no rebase in progress
60 60 [255]
61 61
62 62 Conflicting rebase:
63 63
64 64 $ hg rebase -s 3 -d 2
65 65 merging common
66 66 warning: conflicts during merge.
67 67 merging common incomplete! (edit conflicts, then use 'hg resolve --mark')
68 68 unresolved conflicts (see hg resolve, then hg rebase --continue)
69 69 [1]
70 70
71 71 Try to continue without solving the conflict:
72 72
73 73 $ hg rebase --continue
74 74 abort: unresolved merge conflicts (see hg help resolve)
75 75 [255]
76 76
77 77 Conclude rebase:
78 78
79 79 $ echo 'resolved merge' >common
80 80 $ hg resolve -m common
81 81 $ hg rebase --continue
82 82 saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
83 83
84 84 $ hg tglog
85 85 @ 5:secret 'L3' mybook
86 86 |
87 87 o 4:secret 'L2'
88 88 |
89 89 o 3:draft 'L1'
90 90 |
91 91 o 2:draft 'C3'
92 92 |
93 93 o 1:draft 'C2'
94 94 |
95 95 o 0:draft 'C1'
96 96
97 97 Check correctness:
98 98
99 99 $ hg cat -r 0 common
100 100 c1
101 101
102 102 $ hg cat -r 1 common
103 103 c1
104 104 c2
105 105
106 106 $ hg cat -r 2 common
107 107 c1
108 108 c2
109 109 c3
110 110
111 111 $ hg cat -r 3 common
112 112 c1
113 113 c2
114 114 c3
115 115
116 116 $ hg cat -r 4 common
117 117 resolved merge
118 118
119 119 $ hg cat -r 5 common
120 120 resolved merge
121 121
122 122 Bookmark stays active after --continue
123 123 $ hg bookmarks
124 124 * mybook 5:d67b21408fc0
125 125
126 126 $ cd ..
127
128 Check that the right ancestors is used while rebasing a merge (issue4041)
129
130 $ hg clone "$TESTDIR/bundles/issue4041.hg" issue4041
131 requesting all changes
132 adding changesets
133 adding manifests
134 adding file changes
135 added 11 changesets with 8 changes to 3 files (+1 heads)
136 updating to branch default
137 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
138 $ cd issue4041
139 $ hg phase --draft --force 9
140 $ hg log -G
141 o changeset: 10:2f2496ddf49d
142 |\ branch: f1
143 | | tag: tip
144 | | parent: 7:4c9fbe56a16f
145 | | parent: 9:e31216eec445
146 | | user: szhang
147 | | date: Thu Sep 05 12:59:39 2013 -0400
148 | | summary: merge
149 | |
150 | o changeset: 9:e31216eec445
151 | | branch: f1
152 | | user: szhang
153 | | date: Thu Sep 05 12:59:10 2013 -0400
154 | | summary: more changes to f1
155 | |
156 | o changeset: 8:8e4e2c1a07ae
157 | |\ branch: f1
158 | | | parent: 2:4bc80088dc6b
159 | | | parent: 6:400110238667
160 | | | user: szhang
161 | | | date: Thu Sep 05 12:57:59 2013 -0400
162 | | | summary: bad merge
163 | | |
164 o | | changeset: 7:4c9fbe56a16f
165 |/ / branch: f1
166 | | parent: 2:4bc80088dc6b
167 | | user: szhang
168 | | date: Thu Sep 05 12:54:00 2013 -0400
169 | | summary: changed f1
170 | |
171 | o changeset: 6:400110238667
172 | | branch: f2
173 | | parent: 4:12e8ec6bb010
174 | | user: szhang
175 | | date: Tue Sep 03 13:58:02 2013 -0400
176 | | summary: changed f2 on f2
177 | |
178 | | @ changeset: 5:d79e2059b5c0
179 | | | parent: 3:8a951942e016
180 | | | user: szhang
181 | | | date: Tue Sep 03 13:57:39 2013 -0400
182 | | | summary: changed f2 on default
183 | | |
184 | o | changeset: 4:12e8ec6bb010
185 | |/ branch: f2
186 | | user: szhang
187 | | date: Tue Sep 03 13:57:18 2013 -0400
188 | | summary: created f2 branch
189 | |
190 | o changeset: 3:8a951942e016
191 | | parent: 0:24797d4f68de
192 | | user: szhang
193 | | date: Tue Sep 03 13:57:11 2013 -0400
194 | | summary: added f2.txt
195 | |
196 o | changeset: 2:4bc80088dc6b
197 | | branch: f1
198 | | user: szhang
199 | | date: Tue Sep 03 13:56:20 2013 -0400
200 | | summary: added f1.txt
201 | |
202 o | changeset: 1:ef53c9e6b608
203 |/ branch: f1
204 | user: szhang
205 | date: Tue Sep 03 13:55:26 2013 -0400
206 | summary: created f1 branch
207 |
208 o changeset: 0:24797d4f68de
209 user: szhang
210 date: Tue Sep 03 13:55:08 2013 -0400
211 summary: added default.txt
212
213 $ hg rebase -s9 -d2 --debug # use debug to really check merge base used
214 rebase onto 2 starting from [<changectx e31216eec445>]
215 rebasing: 9:e31216eec445 5/6 changesets (83.33%)
216 future parents are 2 and -1
217 rebase status stored
218 update to 2:4bc80088dc6b
219 resolving manifests
220 branchmerge: False, force: True, partial: False
221 ancestor: d79e2059b5c0+, local: d79e2059b5c0+, remote: 4bc80088dc6b
222 f2.txt: other deleted -> r
223 f1.txt: remote created -> g
224 removing f2.txt
225 updating: f2.txt 1/2 files (50.00%)
226 getting f1.txt
227 updating: f1.txt 2/2 files (100.00%)
228 merge against 9:e31216eec445
229 detach base 8:8e4e2c1a07ae
230 searching for copies back to rev 3
231 resolving manifests
232 branchmerge: True, force: True, partial: False
233 ancestor: 8e4e2c1a07ae, local: 4bc80088dc6b+, remote: e31216eec445
234 f1.txt: remote is newer -> g
235 getting f1.txt
236 updating: f1.txt 1/1 files (100.00%)
237 f1.txt
238 rebasing: 10:2f2496ddf49d 6/6 changesets (100.00%)
239 future parents are 11 and 7
240 rebase status stored
241 already in target
242 merge against 10:2f2496ddf49d
243 detach base 9:e31216eec445
244 searching for copies back to rev 3
245 resolving manifests
246 branchmerge: True, force: True, partial: False
247 ancestor: e31216eec445, local: 19c888675e13+, remote: 2f2496ddf49d
248 f1.txt: remote is newer -> g
249 getting f1.txt
250 updating: f1.txt 1/1 files (100.00%)
251 f1.txt
252 rebase merging completed
253 update back to initial working directory parent
254 resolving manifests
255 branchmerge: False, force: False, partial: False
256 ancestor: 2a7f09cac94c, local: 2a7f09cac94c+, remote: d79e2059b5c0
257 f1.txt: other deleted -> r
258 f2.txt: remote created -> g
259 removing f1.txt
260 updating: f1.txt 1/2 files (50.00%)
261 getting f2.txt
262 updating: f2.txt 2/2 files (100.00%)
263 3 changesets found
264 list of changesets:
265 4c9fbe56a16f30c0d5dcc40ec1a97bbe3325209c
266 e31216eec445e44352c5f01588856059466a24c9
267 2f2496ddf49d69b5ef23ad8cf9fb2e0e4faf0ac2
268 bundling: 1/3 changesets (33.33%)
269 bundling: 2/3 changesets (66.67%)
270 bundling: 3/3 changesets (100.00%)
271 bundling: 1/3 manifests (33.33%)
272 bundling: 2/3 manifests (66.67%)
273 bundling: 3/3 manifests (100.00%)
274 bundling: f1.txt 1/1 files (100.00%)
275 saved backup bundle to $TESTTMP/issue4041/.hg/strip-backup/e31216eec445-backup.hg (glob)
276 3 changesets found
277 list of changesets:
278 4c9fbe56a16f30c0d5dcc40ec1a97bbe3325209c
279 19c888675e133ab5dff84516926a65672eaf04d9
280 2a7f09cac94c7f4b73ebd5cd1a62d3b2e8e336bf
281 bundling: 1/3 changesets (33.33%)
282 bundling: 2/3 changesets (66.67%)
283 bundling: 3/3 changesets (100.00%)
284 bundling: 1/3 manifests (33.33%)
285 bundling: 2/3 manifests (66.67%)
286 bundling: 3/3 manifests (100.00%)
287 bundling: f1.txt 1/1 files (100.00%)
288 adding branch
289 adding changesets
290 changesets: 1 chunks
291 add changeset 4c9fbe56a16f
292 changesets: 2 chunks
293 add changeset 19c888675e13
294 changesets: 3 chunks
295 add changeset 2a7f09cac94c
296 adding manifests
297 manifests: 1/2 chunks (50.00%)
298 manifests: 2/2 chunks (100.00%)
299 manifests: 3/2 chunks (150.00%)
300 adding file changes
301 adding f1.txt revisions
302 files: 1/1 chunks (100.00%)
303 added 2 changesets with 2 changes to 1 files
304 removing unknown node e31216eec445 from 1-phase boundary
305 invalid branchheads cache (served): tip differs
306 rebase completed
307 updating the branch cache
General Comments 0
You need to be logged in to leave comments. Login now