##// END OF EJS Templates
rebase: move base calculation from rebasenode() to defineparents()...
Mads Kiilerich -
r23484:cf3495df default
parent child Browse files
Show More
@@ -1,1037 +1,1039 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 p1, p2 = defineparents(repo, rev, target, state,
374 targetancestors)
373 p1, p2, base = defineparents(repo, rev, target, state,
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 stats = rebasenode(repo, rev, p1, state, collapsef,
384 target)
383 stats = rebasenode(repo, rev, p1, base, state,
384 collapsef, 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 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 p1, p2 = defineparents(repo, min(state), target,
418 state, targetancestors)
417 p1, p2, _base = defineparents(repo, min(state), target,
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 '''Commit the changes and store useful information in extra.
512 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
513 but also store useful information in extra.
513 514 Return node of committed revision.'''
514 515 try:
515 516 repo.dirstate.beginparentchange()
516 517 repo.setparents(repo[p1].node(), repo[p2].node())
517 518 repo.dirstate.endparentchange()
518 519 ctx = repo[rev]
519 520 if commitmsg is None:
520 521 commitmsg = ctx.description()
521 522 extra = {'rebase_source': ctx.hex()}
522 523 if extrafn:
523 524 extrafn(ctx, extra)
524 525
525 526 backup = repo.ui.backupconfig('phases', 'new-commit')
526 527 try:
527 528 targetphase = max(ctx.phase(), phases.draft)
528 529 repo.ui.setconfig('phases', 'new-commit', targetphase, 'rebase')
529 530 # Commit might fail if unresolved files exist
530 531 newnode = repo.commit(text=commitmsg, user=ctx.user(),
531 532 date=ctx.date(), extra=extra, editor=editor)
532 533 finally:
533 534 repo.ui.restoreconfig(backup)
534 535
535 536 repo.dirstate.setbranch(repo[newnode].branch())
536 537 return newnode
537 538 except util.Abort:
538 539 # Invalidate the previous setparents
539 540 repo.dirstate.invalidate()
540 541 raise
541 542
542 def rebasenode(repo, rev, p1, state, collapse, target):
543 'Rebase a single revision'
543 def rebasenode(repo, rev, p1, base, state, collapse, target):
544 'Rebase a single revision rev on top of p1 using base as merge ancestor'
544 545 # Merge phase
545 546 # Update to target and merge it with local
546 547 if repo['.'].rev() != p1:
547 548 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
548 549 merge.update(repo, p1, False, True, False)
549 550 else:
550 551 repo.ui.debug(" already in target\n")
551 552 repo.dirstate.write()
552 553 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
553 if rev == min(state):
554 # Case (1) initial changeset of a non-detaching rebase.
555 # Let the merge mechanism find the base itself.
556 base = None
557 elif not repo[rev].p2():
558 # Case (2) detaching the node with a single parent, use this parent
559 base = repo[rev].p1().rev()
560 else:
561 # In case of merge, we need to pick the right parent as merge base.
562 #
563 # Imagine we have:
564 # - M: currently rebase revision in this step
565 # - A: one parent of M
566 # - B: second parent of M
567 # - D: destination of this merge step (p1 var)
568 #
569 # If we are rebasing on D, D is the successors of A or B. The right
570 # merge base is the one D succeed to. We pretend it is B for the rest
571 # of this comment
572 #
573 # If we pick B as the base, the merge involves:
574 # - changes from B to M (actual changeset payload)
575 # - changes from B to D (induced by rebase) as D is a rebased
576 # version of B)
577 # Which exactly represent the rebase operation.
578 #
579 # If we pick the A as the base, the merge involves
580 # - changes from A to M (actual changeset payload)
581 # - changes from A to D (with include changes between unrelated A and B
582 # plus changes induced by rebase)
583 # Which does not represent anything sensible and creates a lot of
584 # conflicts.
585 for p in repo[rev].parents():
586 if state.get(p.rev()) == p1:
587 base = p.rev()
588 break
589 else: # fallback when base not found
590 base = None
591
592 # Raise because this function is called wrong (see issue 4106)
593 raise AssertionError('no base found to rebase on '
594 '(rebasenode called wrong)')
595 554 if base is not None:
596 555 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
597 556 # When collapsing in-place, the parent is the common ancestor, we
598 557 # have to allow merging with it.
599 558 stats = merge.update(repo, rev, True, True, False, base, collapse,
600 559 labels=['dest', 'source'])
601 560 if collapse:
602 561 copies.duplicatecopies(repo, rev, target)
603 562 else:
604 563 # If we're not using --collapse, we need to
605 564 # duplicate copies between the revision we're
606 565 # rebasing and its first parent, but *not*
607 566 # duplicate any copies that have already been
608 567 # performed in the destination.
609 568 p1rev = repo[rev].p1().rev()
610 569 copies.duplicatecopies(repo, rev, p1rev, skiprev=target)
611 570 return stats
612 571
613 572 def nearestrebased(repo, rev, state):
614 573 """return the nearest ancestors of rev in the rebase result"""
615 574 rebased = [r for r in state if state[r] > nullmerge]
616 575 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
617 576 if candidates:
618 577 return state[candidates.first()]
619 578 else:
620 579 return None
621 580
622 581 def defineparents(repo, rev, target, state, targetancestors):
623 582 'Return the new parent relationship of the revision that will be rebased'
624 583 parents = repo[rev].parents()
625 584 p1 = p2 = nullrev
626 585
627 586 p1n = parents[0].rev()
628 587 if p1n in targetancestors:
629 588 p1 = target
630 589 elif p1n in state:
631 590 if state[p1n] == nullmerge:
632 591 p1 = target
633 592 elif state[p1n] == revignored:
634 593 p1 = nearestrebased(repo, p1n, state)
635 594 if p1 is None:
636 595 p1 = target
637 596 else:
638 597 p1 = state[p1n]
639 598 else: # p1n external
640 599 p1 = target
641 600 p2 = p1n
642 601
643 602 if len(parents) == 2 and parents[1].rev() not in targetancestors:
644 603 p2n = parents[1].rev()
645 604 # interesting second parent
646 605 if p2n in state:
647 606 if p1 == target: # p1n in targetancestors or external
648 607 p1 = state[p2n]
649 608 elif state[p2n] == revignored:
650 609 p2 = nearestrebased(repo, p2n, state)
651 610 if p2 is None:
652 611 # no ancestors rebased yet, detach
653 612 p2 = target
654 613 else:
655 614 p2 = state[p2n]
656 615 else: # p2n external
657 616 if p2 != nullrev: # p1n external too => rev is a merged revision
658 617 raise util.Abort(_('cannot use revision %d as base, result '
659 618 'would have 3 parents') % rev)
660 619 p2 = p2n
661 620 repo.ui.debug(" future parents are %d and %d\n" %
662 621 (repo[p1].rev(), repo[p2].rev()))
663 return p1, p2
622
623 if rev == min(state):
624 # Case (1) initial changeset of a non-detaching rebase.
625 # Let the merge mechanism find the base itself.
626 base = None
627 elif not repo[rev].p2():
628 # Case (2) detaching the node with a single parent, use this parent
629 base = repo[rev].p1().rev()
630 else:
631 # In case of merge, we need to pick the right parent as merge base.
632 #
633 # Imagine we have:
634 # - M: currently rebase revision in this step
635 # - A: one parent of M
636 # - B: second parent of M
637 # - D: destination of this merge step (p1 var)
638 #
639 # If we are rebasing on D, D is the successors of A or B. The right
640 # merge base is the one D succeed to. We pretend it is B for the rest
641 # of this comment
642 #
643 # If we pick B as the base, the merge involves:
644 # - changes from B to M (actual changeset payload)
645 # - changes from B to D (induced by rebase) as D is a rebased
646 # version of B)
647 # Which exactly represent the rebase operation.
648 #
649 # If we pick the A as the base, the merge involves
650 # - changes from A to M (actual changeset payload)
651 # - changes from A to D (with include changes between unrelated A and B
652 # plus changes induced by rebase)
653 # Which does not represent anything sensible and creates a lot of
654 # conflicts.
655 for p in repo[rev].parents():
656 if state.get(p.rev()) == p1:
657 base = p.rev()
658 break
659 else: # fallback when base not found
660 base = None
661
662 # Raise because this function is called wrong (see issue 4106)
663 raise AssertionError('no base found to rebase on '
664 '(defineparents called wrong)')
665 return p1, p2, base
664 666
665 667 def isagitpatch(repo, patchname):
666 668 'Return true if the given patch is in git format'
667 669 mqpatch = os.path.join(repo.mq.path, patchname)
668 670 for line in patch.linereader(file(mqpatch, 'rb')):
669 671 if line.startswith('diff --git'):
670 672 return True
671 673 return False
672 674
673 675 def updatemq(repo, state, skipped, **opts):
674 676 'Update rebased mq patches - finalize and then import them'
675 677 mqrebase = {}
676 678 mq = repo.mq
677 679 original_series = mq.fullseries[:]
678 680 skippedpatches = set()
679 681
680 682 for p in mq.applied:
681 683 rev = repo[p.node].rev()
682 684 if rev in state:
683 685 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
684 686 (rev, p.name))
685 687 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
686 688 else:
687 689 # Applied but not rebased, not sure this should happen
688 690 skippedpatches.add(p.name)
689 691
690 692 if mqrebase:
691 693 mq.finish(repo, mqrebase.keys())
692 694
693 695 # We must start import from the newest revision
694 696 for rev in sorted(mqrebase, reverse=True):
695 697 if rev not in skipped:
696 698 name, isgit = mqrebase[rev]
697 699 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
698 700 mq.qimport(repo, (), patchname=name, git=isgit,
699 701 rev=[str(state[rev])])
700 702 else:
701 703 # Rebased and skipped
702 704 skippedpatches.add(mqrebase[rev][0])
703 705
704 706 # Patches were either applied and rebased and imported in
705 707 # order, applied and removed or unapplied. Discard the removed
706 708 # ones while preserving the original series order and guards.
707 709 newseries = [s for s in original_series
708 710 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
709 711 mq.fullseries[:] = newseries
710 712 mq.seriesdirty = True
711 713 mq.savedirty()
712 714
713 715 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
714 716 'Move bookmarks to their correct changesets, and delete divergent ones'
715 717 marks = repo._bookmarks
716 718 for k, v in originalbookmarks.iteritems():
717 719 if v in nstate:
718 720 # update the bookmarks for revs that have moved
719 721 marks[k] = nstate[v]
720 722 bookmarks.deletedivergent(repo, [targetnode], k)
721 723
722 724 marks.write()
723 725
724 726 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
725 727 external, activebookmark):
726 728 'Store the current status to allow recovery'
727 729 f = repo.opener("rebasestate", "w")
728 730 f.write(repo[originalwd].hex() + '\n')
729 731 f.write(repo[target].hex() + '\n')
730 732 f.write(repo[external].hex() + '\n')
731 733 f.write('%d\n' % int(collapse))
732 734 f.write('%d\n' % int(keep))
733 735 f.write('%d\n' % int(keepbranches))
734 736 f.write('%s\n' % (activebookmark or ''))
735 737 for d, v in state.iteritems():
736 738 oldrev = repo[d].hex()
737 739 if v > nullmerge:
738 740 newrev = repo[v].hex()
739 741 else:
740 742 newrev = v
741 743 f.write("%s:%s\n" % (oldrev, newrev))
742 744 f.close()
743 745 repo.ui.debug('rebase status stored\n')
744 746
745 747 def clearstatus(repo):
746 748 'Remove the status files'
747 749 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
748 750
749 751 def restorestatus(repo):
750 752 'Restore a previously stored status'
751 753 try:
752 754 keepbranches = None
753 755 target = None
754 756 collapse = False
755 757 external = nullrev
756 758 activebookmark = None
757 759 state = {}
758 760 f = repo.opener("rebasestate")
759 761 for i, l in enumerate(f.read().splitlines()):
760 762 if i == 0:
761 763 originalwd = repo[l].rev()
762 764 elif i == 1:
763 765 target = repo[l].rev()
764 766 elif i == 2:
765 767 external = repo[l].rev()
766 768 elif i == 3:
767 769 collapse = bool(int(l))
768 770 elif i == 4:
769 771 keep = bool(int(l))
770 772 elif i == 5:
771 773 keepbranches = bool(int(l))
772 774 elif i == 6 and not (len(l) == 81 and ':' in l):
773 775 # line 6 is a recent addition, so for backwards compatibility
774 776 # check that the line doesn't look like the oldrev:newrev lines
775 777 activebookmark = l
776 778 else:
777 779 oldrev, newrev = l.split(':')
778 780 if newrev in (str(nullmerge), str(revignored)):
779 781 state[repo[oldrev].rev()] = int(newrev)
780 782 else:
781 783 state[repo[oldrev].rev()] = repo[newrev].rev()
782 784
783 785 if keepbranches is None:
784 786 raise util.Abort(_('.hg/rebasestate is incomplete'))
785 787
786 788 skipped = set()
787 789 # recompute the set of skipped revs
788 790 if not collapse:
789 791 seen = set([target])
790 792 for old, new in sorted(state.items()):
791 793 if new != nullrev and new in seen:
792 794 skipped.add(old)
793 795 seen.add(new)
794 796 repo.ui.debug('computed skipped revs: %s\n' %
795 797 (' '.join(str(r) for r in sorted(skipped)) or None))
796 798 repo.ui.debug('rebase status resumed\n')
797 799 return (originalwd, target, state, skipped,
798 800 collapse, keep, keepbranches, external, activebookmark)
799 801 except IOError, err:
800 802 if err.errno != errno.ENOENT:
801 803 raise
802 804 raise util.Abort(_('no rebase in progress'))
803 805
804 806 def inrebase(repo, originalwd, state):
805 807 '''check whether the working dir is in an interrupted rebase'''
806 808 parents = [p.rev() for p in repo.parents()]
807 809 if originalwd in parents:
808 810 return True
809 811
810 812 for newrev in state.itervalues():
811 813 if newrev in parents:
812 814 return True
813 815
814 816 return False
815 817
816 818 def abort(repo, originalwd, target, state):
817 819 'Restore the repository to its original state'
818 820 dstates = [s for s in state.values() if s > nullrev]
819 821 immutable = [d for d in dstates if not repo[d].mutable()]
820 822 cleanup = True
821 823 if immutable:
822 824 repo.ui.warn(_("warning: can't clean up immutable changesets %s\n")
823 825 % ', '.join(str(repo[r]) for r in immutable),
824 826 hint=_('see hg help phases for details'))
825 827 cleanup = False
826 828
827 829 descendants = set()
828 830 if dstates:
829 831 descendants = set(repo.changelog.descendants(dstates))
830 832 if descendants - set(dstates):
831 833 repo.ui.warn(_("warning: new changesets detected on target branch, "
832 834 "can't strip\n"))
833 835 cleanup = False
834 836
835 837 if cleanup:
836 838 # Update away from the rebase if necessary
837 839 if inrebase(repo, originalwd, state):
838 840 merge.update(repo, originalwd, False, True, False)
839 841
840 842 # Strip from the first rebased revision
841 843 rebased = filter(lambda x: x > -1 and x != target, state.values())
842 844 if rebased:
843 845 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
844 846 # no backup of rebased cset versions needed
845 847 repair.strip(repo.ui, repo, strippoints)
846 848
847 849 clearstatus(repo)
848 850 repo.ui.warn(_('rebase aborted\n'))
849 851 return 0
850 852
851 853 def buildstate(repo, dest, rebaseset, collapse):
852 854 '''Define which revisions are going to be rebased and where
853 855
854 856 repo: repo
855 857 dest: context
856 858 rebaseset: set of rev
857 859 '''
858 860
859 861 # This check isn't strictly necessary, since mq detects commits over an
860 862 # applied patch. But it prevents messing up the working directory when
861 863 # a partially completed rebase is blocked by mq.
862 864 if 'qtip' in repo.tags() and (dest.node() in
863 865 [s.node for s in repo.mq.applied]):
864 866 raise util.Abort(_('cannot rebase onto an applied mq patch'))
865 867
866 868 roots = list(repo.set('roots(%ld)', rebaseset))
867 869 if not roots:
868 870 raise util.Abort(_('no matching revisions'))
869 871 roots.sort()
870 872 state = {}
871 873 detachset = set()
872 874 for root in roots:
873 875 commonbase = root.ancestor(dest)
874 876 if commonbase == root:
875 877 raise util.Abort(_('source is ancestor of destination'))
876 878 if commonbase == dest:
877 879 samebranch = root.branch() == dest.branch()
878 880 if not collapse and samebranch and root in dest.children():
879 881 repo.ui.debug('source is a child of destination\n')
880 882 return None
881 883
882 884 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, root))
883 885 state.update(dict.fromkeys(rebaseset, nullrev))
884 886 # Rebase tries to turn <dest> into a parent of <root> while
885 887 # preserving the number of parents of rebased changesets:
886 888 #
887 889 # - A changeset with a single parent will always be rebased as a
888 890 # changeset with a single parent.
889 891 #
890 892 # - A merge will be rebased as merge unless its parents are both
891 893 # ancestors of <dest> or are themselves in the rebased set and
892 894 # pruned while rebased.
893 895 #
894 896 # If one parent of <root> is an ancestor of <dest>, the rebased
895 897 # version of this parent will be <dest>. This is always true with
896 898 # --base option.
897 899 #
898 900 # Otherwise, we need to *replace* the original parents with
899 901 # <dest>. This "detaches" the rebased set from its former location
900 902 # and rebases it onto <dest>. Changes introduced by ancestors of
901 903 # <root> not common with <dest> (the detachset, marked as
902 904 # nullmerge) are "removed" from the rebased changesets.
903 905 #
904 906 # - If <root> has a single parent, set it to <dest>.
905 907 #
906 908 # - If <root> is a merge, we cannot decide which parent to
907 909 # replace, the rebase operation is not clearly defined.
908 910 #
909 911 # The table below sums up this behavior:
910 912 #
911 913 # +------------------+----------------------+-------------------------+
912 914 # | | one parent | merge |
913 915 # +------------------+----------------------+-------------------------+
914 916 # | parent in | new parent is <dest> | parents in ::<dest> are |
915 917 # | ::<dest> | | remapped to <dest> |
916 918 # +------------------+----------------------+-------------------------+
917 919 # | unrelated source | new parent is <dest> | ambiguous, abort |
918 920 # +------------------+----------------------+-------------------------+
919 921 #
920 922 # The actual abort is handled by `defineparents`
921 923 if len(root.parents()) <= 1:
922 924 # ancestors of <root> not ancestors of <dest>
923 925 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
924 926 [root.rev()]))
925 927 for r in detachset:
926 928 if r not in state:
927 929 state[r] = nullmerge
928 930 if len(roots) > 1:
929 931 # If we have multiple roots, we may have "hole" in the rebase set.
930 932 # Rebase roots that descend from those "hole" should not be detached as
931 933 # other root are. We use the special `revignored` to inform rebase that
932 934 # the revision should be ignored but that `defineparents` should search
933 935 # a rebase destination that make sense regarding rebased topology.
934 936 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
935 937 for ignored in set(rebasedomain) - set(rebaseset):
936 938 state[ignored] = revignored
937 939 return repo['.'].rev(), dest.rev(), state
938 940
939 941 def clearrebased(ui, repo, state, skipped, collapsedas=None):
940 942 """dispose of rebased revision at the end of the rebase
941 943
942 944 If `collapsedas` is not None, the rebase was a collapse whose result if the
943 945 `collapsedas` node."""
944 946 if obsolete.isenabled(repo, obsolete.createmarkersopt):
945 947 markers = []
946 948 for rev, newrev in sorted(state.items()):
947 949 if newrev >= 0:
948 950 if rev in skipped:
949 951 succs = ()
950 952 elif collapsedas is not None:
951 953 succs = (repo[collapsedas],)
952 954 else:
953 955 succs = (repo[newrev],)
954 956 markers.append((repo[rev], succs))
955 957 if markers:
956 958 obsolete.createmarkers(repo, markers)
957 959 else:
958 960 rebased = [rev for rev in state if state[rev] > nullmerge]
959 961 if rebased:
960 962 stripped = []
961 963 for root in repo.set('roots(%ld)', rebased):
962 964 if set(repo.changelog.descendants([root.rev()])) - set(state):
963 965 ui.warn(_("warning: new changesets detected "
964 966 "on source branch, not stripping\n"))
965 967 else:
966 968 stripped.append(root.node())
967 969 if stripped:
968 970 # backup the old csets by default
969 971 repair.strip(ui, repo, stripped, "all")
970 972
971 973
972 974 def pullrebase(orig, ui, repo, *args, **opts):
973 975 'Call rebase after pull if the latter has been invoked with --rebase'
974 976 if opts.get('rebase'):
975 977 if opts.get('update'):
976 978 del opts['update']
977 979 ui.debug('--update and --rebase are not compatible, ignoring '
978 980 'the update flag\n')
979 981
980 982 movemarkfrom = repo['.'].node()
981 983 revsprepull = len(repo)
982 984 origpostincoming = commands.postincoming
983 985 def _dummy(*args, **kwargs):
984 986 pass
985 987 commands.postincoming = _dummy
986 988 try:
987 989 orig(ui, repo, *args, **opts)
988 990 finally:
989 991 commands.postincoming = origpostincoming
990 992 revspostpull = len(repo)
991 993 if revspostpull > revsprepull:
992 994 # --rev option from pull conflict with rebase own --rev
993 995 # dropping it
994 996 if 'rev' in opts:
995 997 del opts['rev']
996 998 rebase(ui, repo, **opts)
997 999 branch = repo[None].branch()
998 1000 dest = repo[branch].rev()
999 1001 if dest != repo['.'].rev():
1000 1002 # there was nothing to rebase we force an update
1001 1003 hg.update(repo, dest)
1002 1004 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
1003 1005 ui.status(_("updating bookmark %s\n")
1004 1006 % repo._bookmarkcurrent)
1005 1007 else:
1006 1008 if opts.get('tool'):
1007 1009 raise util.Abort(_('--tool can only be used with --rebase'))
1008 1010 orig(ui, repo, *args, **opts)
1009 1011
1010 1012 def summaryhook(ui, repo):
1011 1013 if not os.path.exists(repo.join('rebasestate')):
1012 1014 return
1013 1015 try:
1014 1016 state = restorestatus(repo)[2]
1015 1017 except error.RepoLookupError:
1016 1018 # i18n: column positioning for "hg summary"
1017 1019 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1018 1020 ui.write(msg)
1019 1021 return
1020 1022 numrebased = len([i for i in state.itervalues() if i != -1])
1021 1023 # i18n: column positioning for "hg summary"
1022 1024 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1023 1025 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1024 1026 ui.label(_('%d remaining'), 'rebase.remaining') %
1025 1027 (len(state) - numrebased)))
1026 1028
1027 1029 def uisetup(ui):
1028 1030 'Replace pull with a decorator to provide --rebase option'
1029 1031 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1030 1032 entry[1].append(('', 'rebase', None,
1031 1033 _("rebase working directory to branch head")))
1032 1034 entry[1].append(('t', 'tool', '',
1033 1035 _("specify merge tool for rebase")))
1034 1036 cmdutil.summaryhooks.add('rebase', summaryhook)
1035 1037 cmdutil.unfinishedstates.append(
1036 1038 ['rebasestate', False, False, _('rebase in progress'),
1037 1039 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now