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