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