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