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