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