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