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