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