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