##// END OF EJS Templates
rebase: small refactoring to allow better extensibility from extensions...
Laurent Charignon -
r27790:2d294dad default
parent child Browse files
Show More
@@ -1,1258 +1,1261 b''
1 1 # rebase.py - rebasing feature for mercurial
2 2 #
3 3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to move sets of revisions to a different ancestor
9 9
10 10 This extension lets you rebase changesets in an existing Mercurial
11 11 repository.
12 12
13 13 For more information:
14 14 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 rebaseobsrevs = _filterobsoleterevs(repo, rebasesetrevs)
348 347 obsoletenotrebased = _computeobsoletenotrebased(repo,
349 348 rebaseobsrevs,
350 349 dest)
351 350 rebaseobsskipped = set(obsoletenotrebased)
352 351
353 352 # Obsolete node with successors not in dest leads to divergence
354 353 divergenceok = ui.configbool('rebase',
355 354 'allowdivergence')
356 355 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
357 356
358 357 if divergencebasecandidates and not divergenceok:
359 358 msg = _("this rebase will cause divergence")
360 359 h = _("to force the rebase please set "
361 360 "rebase.allowdivergence=True")
362 361 raise error.Abort(msg, hint=h)
363 362
364 363 # - plain prune (no successor) changesets are rebased
365 364 # - split changesets are not rebased if at least one of the
366 365 # changeset resulting from the split is an ancestor of dest
367 366 rebaseset = rebasesetrevs - rebaseobsskipped
368 367 if rebasesetrevs and not rebaseset:
369 368 msg = _('all requested changesets have equivalents '
370 369 'or were marked as obsolete')
371 370 hint = _('to force the rebase, set the config '
372 371 'experimental.rebaseskipobsolete to False')
373 372 raise error.Abort(msg, hint=hint)
374 373
375 374 result = buildstate(repo, dest, rebaseset, collapsef,
376 375 obsoletenotrebased)
377 376
378 377 if not result:
379 378 # Empty state built, nothing to rebase
380 379 ui.status(_('nothing to rebase\n'))
381 380 return _nothingtorebase()
382 381
383 382 root = min(rebaseset)
384 383 if not keepf and not repo[root].mutable():
385 384 raise error.Abort(_("can't rebase public changeset %s")
386 385 % repo[root],
387 386 hint=_('see "hg help phases" for details'))
388 387
389 388 originalwd, target, state = result
390 389 if collapsef:
391 390 targetancestors = repo.changelog.ancestors([target],
392 391 inclusive=True)
393 392 external = externalparent(repo, state, targetancestors)
394 393
395 394 if dest.closesbranch() and not keepbranchesf:
396 395 ui.status(_('reopening closed branch head %s\n') % dest)
397 396
398 397 if keepbranchesf and collapsef:
399 398 branches = set()
400 399 for rev in state:
401 400 branches.add(repo[rev].branch())
402 401 if len(branches) > 1:
403 402 raise error.Abort(_('cannot collapse multiple named '
404 403 'branches'))
405 404
406 405 # Rebase
407 406 if not targetancestors:
408 407 targetancestors = repo.changelog.ancestors([target], inclusive=True)
409 408
410 409 # Keep track of the current bookmarks in order to reset them later
411 410 currentbookmarks = repo._bookmarks.copy()
412 411 activebookmark = activebookmark or repo._activebookmark
413 412 if activebookmark:
414 413 bookmarks.deactivate(repo)
415 414
416 415 extrafn = _makeextrafn(extrafns)
417 416
418 417 sortedstate = sorted(state)
419 418 total = len(sortedstate)
420 419 pos = 0
421 420 for rev in sortedstate:
422 421 ctx = repo[rev]
423 422 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
424 423 ctx.description().split('\n', 1)[0])
425 424 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
426 425 if names:
427 426 desc += ' (%s)' % ' '.join(names)
428 427 pos += 1
429 428 if state[rev] == revtodo:
430 429 ui.status(_('rebasing %s\n') % desc)
431 430 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
432 431 _('changesets'), total)
433 432 p1, p2, base = defineparents(repo, rev, target, state,
434 433 targetancestors)
435 434 storestatus(repo, originalwd, target, state, collapsef, keepf,
436 435 keepbranchesf, external, activebookmark)
437 436 if len(repo[None].parents()) == 2:
438 437 repo.ui.debug('resuming interrupted rebase\n')
439 438 else:
440 439 try:
441 440 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
442 441 'rebase')
443 442 stats = rebasenode(repo, rev, p1, base, state,
444 443 collapsef, target)
445 444 if stats and stats[3] > 0:
446 445 raise error.InterventionRequired(
447 446 _('unresolved conflicts (see hg '
448 447 'resolve, then hg rebase --continue)'))
449 448 finally:
450 449 ui.setconfig('ui', 'forcemerge', '', 'rebase')
451 450 if not collapsef:
452 451 merging = p2 != nullrev
453 452 editform = cmdutil.mergeeditform(merging, 'rebase')
454 453 editor = cmdutil.getcommiteditor(editform=editform, **opts)
455 454 newnode = concludenode(repo, rev, p1, p2, extrafn=extrafn,
456 455 editor=editor,
457 456 keepbranches=keepbranchesf,
458 457 date=date)
459 458 else:
460 459 # Skip commit if we are collapsing
461 460 repo.dirstate.beginparentchange()
462 461 repo.setparents(repo[p1].node())
463 462 repo.dirstate.endparentchange()
464 463 newnode = None
465 464 # Update the state
466 465 if newnode is not None:
467 466 state[rev] = repo[newnode].rev()
468 467 ui.debug('rebased as %s\n' % short(newnode))
469 468 else:
470 469 if not collapsef:
471 470 ui.warn(_('note: rebase of %d:%s created no changes '
472 471 'to commit\n') % (rev, ctx))
473 472 skipped.add(rev)
474 473 state[rev] = p1
475 474 ui.debug('next revision set to %s\n' % p1)
476 475 elif state[rev] == nullmerge:
477 476 ui.debug('ignoring null merge rebase of %s\n' % rev)
478 477 elif state[rev] == revignored:
479 478 ui.status(_('not rebasing ignored %s\n') % desc)
480 479 elif state[rev] == revprecursor:
481 480 targetctx = repo[obsoletenotrebased[rev]]
482 481 desctarget = '%d:%s "%s"' % (targetctx.rev(), targetctx,
483 482 targetctx.description().split('\n', 1)[0])
484 483 msg = _('note: not rebasing %s, already in destination as %s\n')
485 484 ui.status(msg % (desc, desctarget))
486 485 elif state[rev] == revpruned:
487 486 msg = _('note: not rebasing %s, it has no successor\n')
488 487 ui.status(msg % desc)
489 488 else:
490 489 ui.status(_('already rebased %s as %s\n') %
491 490 (desc, repo[state[rev]]))
492 491
493 492 ui.progress(_('rebasing'), None)
494 493 ui.note(_('rebase merging completed\n'))
495 494
496 495 if collapsef and not keepopen:
497 496 p1, p2, _base = defineparents(repo, min(state), target,
498 497 state, targetancestors)
499 498 editopt = opts.get('edit')
500 499 editform = 'rebase.collapse'
501 500 if collapsemsg:
502 501 commitmsg = collapsemsg
503 502 else:
504 503 commitmsg = 'Collapsed revision'
505 504 for rebased in state:
506 505 if rebased not in skipped and state[rebased] > nullmerge:
507 506 commitmsg += '\n* %s' % repo[rebased].description()
508 507 editopt = True
509 508 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
510 509 newnode = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
511 510 extrafn=extrafn, editor=editor,
512 511 keepbranches=keepbranchesf,
513 512 date=date)
514 513 if newnode is None:
515 514 newrev = target
516 515 else:
517 516 newrev = repo[newnode].rev()
518 517 for oldrev in state.iterkeys():
519 518 if state[oldrev] > nullmerge:
520 519 state[oldrev] = newrev
521 520
522 521 if 'qtip' in repo.tags():
523 522 updatemq(repo, state, skipped, **opts)
524 523
525 524 if currentbookmarks:
526 525 # Nodeids are needed to reset bookmarks
527 526 nstate = {}
528 527 for k, v in state.iteritems():
529 528 if v > nullmerge:
530 529 nstate[repo[k].node()] = repo[v].node()
531 530 # XXX this is the same as dest.node() for the non-continue path --
532 531 # this should probably be cleaned up
533 532 targetnode = repo[target].node()
534 533
535 534 # restore original working directory
536 535 # (we do this before stripping)
537 536 newwd = state.get(originalwd, originalwd)
538 537 if newwd < 0:
539 538 # original directory is a parent of rebase set root or ignored
540 539 newwd = originalwd
541 540 if newwd not in [c.rev() for c in repo[None].parents()]:
542 541 ui.note(_("update back to initial working directory parent\n"))
543 542 hg.updaterepo(repo, newwd, False)
544 543
545 544 if not keepf:
546 545 collapsedas = None
547 546 if collapsef:
548 547 collapsedas = newnode
549 548 clearrebased(ui, repo, state, skipped, collapsedas)
550 549
551 550 tr = None
552 551 try:
553 552 tr = repo.transaction('bookmark')
554 553 if currentbookmarks:
555 554 updatebookmarks(repo, targetnode, nstate, currentbookmarks, tr)
556 555 if activebookmark not in repo._bookmarks:
557 556 # active bookmark was divergent one and has been deleted
558 557 activebookmark = None
559 558 tr.close()
560 559 finally:
561 560 release(tr)
562 561 clearstatus(repo)
563 562
564 563 ui.note(_("rebase completed\n"))
565 564 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
566 565 if skipped:
567 566 ui.note(_("%d revisions have been skipped\n") % len(skipped))
568 567
569 568 if (activebookmark and
570 569 repo['.'].node() == repo._bookmarks[activebookmark]):
571 570 bookmarks.activate(repo, activebookmark)
572 571
573 572 finally:
574 573 release(lock, wlock)
575 574
576 575 def externalparent(repo, state, targetancestors):
577 576 """Return the revision that should be used as the second parent
578 577 when the revisions in state is collapsed on top of targetancestors.
579 578 Abort if there is more than one parent.
580 579 """
581 580 parents = set()
582 581 source = min(state)
583 582 for rev in state:
584 583 if rev == source:
585 584 continue
586 585 for p in repo[rev].parents():
587 586 if (p.rev() not in state
588 587 and p.rev() not in targetancestors):
589 588 parents.add(p.rev())
590 589 if not parents:
591 590 return nullrev
592 591 if len(parents) == 1:
593 592 return parents.pop()
594 593 raise error.Abort(_('unable to collapse on top of %s, there is more '
595 594 'than one external parent: %s') %
596 595 (max(targetancestors),
597 596 ', '.join(str(p) for p in sorted(parents))))
598 597
599 598 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None,
600 599 keepbranches=False, date=None):
601 600 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
602 601 but also store useful information in extra.
603 602 Return node of committed revision.'''
604 603 dsguard = cmdutil.dirstateguard(repo, 'rebase')
605 604 try:
606 605 repo.setparents(repo[p1].node(), repo[p2].node())
607 606 ctx = repo[rev]
608 607 if commitmsg is None:
609 608 commitmsg = ctx.description()
610 609 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
611 610 extra = ctx.extra().copy()
612 611 if not keepbranches:
613 612 del extra['branch']
614 613 extra['rebase_source'] = ctx.hex()
615 614 if extrafn:
616 615 extrafn(ctx, extra)
617 616
618 617 backup = repo.ui.backupconfig('phases', 'new-commit')
619 618 try:
620 619 targetphase = max(ctx.phase(), phases.draft)
621 620 repo.ui.setconfig('phases', 'new-commit', targetphase, 'rebase')
622 621 if keepbranch:
623 622 repo.ui.setconfig('ui', 'allowemptycommit', True)
624 623 # Commit might fail if unresolved files exist
625 624 if date is None:
626 625 date = ctx.date()
627 626 newnode = repo.commit(text=commitmsg, user=ctx.user(),
628 627 date=date, extra=extra, editor=editor)
629 628 finally:
630 629 repo.ui.restoreconfig(backup)
631 630
632 631 repo.dirstate.setbranch(repo[newnode].branch())
633 632 dsguard.close()
634 633 return newnode
635 634 finally:
636 635 release(dsguard)
637 636
638 637 def rebasenode(repo, rev, p1, base, state, collapse, target):
639 638 'Rebase a single revision rev on top of p1 using base as merge ancestor'
640 639 # Merge phase
641 640 # Update to target and merge it with local
642 641 if repo['.'].rev() != p1:
643 642 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
644 643 merge.update(repo, p1, False, True)
645 644 else:
646 645 repo.ui.debug(" already in target\n")
647 646 repo.dirstate.write(repo.currenttransaction())
648 647 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
649 648 if base is not None:
650 649 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
651 650 # When collapsing in-place, the parent is the common ancestor, we
652 651 # have to allow merging with it.
653 652 stats = merge.update(repo, rev, True, True, base, collapse,
654 653 labels=['dest', 'source'])
655 654 if collapse:
656 655 copies.duplicatecopies(repo, rev, target)
657 656 else:
658 657 # If we're not using --collapse, we need to
659 658 # duplicate copies between the revision we're
660 659 # rebasing and its first parent, but *not*
661 660 # duplicate any copies that have already been
662 661 # performed in the destination.
663 662 p1rev = repo[rev].p1().rev()
664 663 copies.duplicatecopies(repo, rev, p1rev, skiprev=target)
665 664 return stats
666 665
667 666 def nearestrebased(repo, rev, state):
668 667 """return the nearest ancestors of rev in the rebase result"""
669 668 rebased = [r for r in state if state[r] > nullmerge]
670 669 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
671 670 if candidates:
672 671 return state[candidates.first()]
673 672 else:
674 673 return None
675 674
676 675 def defineparents(repo, rev, target, state, targetancestors):
677 676 'Return the new parent relationship of the revision that will be rebased'
678 677 parents = repo[rev].parents()
679 678 p1 = p2 = nullrev
680 679
681 680 p1n = parents[0].rev()
682 681 if p1n in targetancestors:
683 682 p1 = target
684 683 elif p1n in state:
685 684 if state[p1n] == nullmerge:
686 685 p1 = target
687 686 elif state[p1n] in revskipped:
688 687 p1 = nearestrebased(repo, p1n, state)
689 688 if p1 is None:
690 689 p1 = target
691 690 else:
692 691 p1 = state[p1n]
693 692 else: # p1n external
694 693 p1 = target
695 694 p2 = p1n
696 695
697 696 if len(parents) == 2 and parents[1].rev() not in targetancestors:
698 697 p2n = parents[1].rev()
699 698 # interesting second parent
700 699 if p2n in state:
701 700 if p1 == target: # p1n in targetancestors or external
702 701 p1 = state[p2n]
703 702 elif state[p2n] in revskipped:
704 703 p2 = nearestrebased(repo, p2n, state)
705 704 if p2 is None:
706 705 # no ancestors rebased yet, detach
707 706 p2 = target
708 707 else:
709 708 p2 = state[p2n]
710 709 else: # p2n external
711 710 if p2 != nullrev: # p1n external too => rev is a merged revision
712 711 raise error.Abort(_('cannot use revision %d as base, result '
713 712 'would have 3 parents') % rev)
714 713 p2 = p2n
715 714 repo.ui.debug(" future parents are %d and %d\n" %
716 715 (repo[p1].rev(), repo[p2].rev()))
717 716
718 717 if rev == min(state):
719 718 # Case (1) initial changeset of a non-detaching rebase.
720 719 # Let the merge mechanism find the base itself.
721 720 base = None
722 721 elif not repo[rev].p2():
723 722 # Case (2) detaching the node with a single parent, use this parent
724 723 base = repo[rev].p1().rev()
725 724 else:
726 725 # Assuming there is a p1, this is the case where there also is a p2.
727 726 # We are thus rebasing a merge and need to pick the right merge base.
728 727 #
729 728 # Imagine we have:
730 729 # - M: current rebase revision in this step
731 730 # - A: one parent of M
732 731 # - B: other parent of M
733 732 # - D: destination of this merge step (p1 var)
734 733 #
735 734 # Consider the case where D is a descendant of A or B and the other is
736 735 # 'outside'. In this case, the right merge base is the D ancestor.
737 736 #
738 737 # An informal proof, assuming A is 'outside' and B is the D ancestor:
739 738 #
740 739 # If we pick B as the base, the merge involves:
741 740 # - changes from B to M (actual changeset payload)
742 741 # - changes from B to D (induced by rebase) as D is a rebased
743 742 # version of B)
744 743 # Which exactly represent the rebase operation.
745 744 #
746 745 # If we pick A as the base, the merge involves:
747 746 # - changes from A to M (actual changeset payload)
748 747 # - changes from A to D (with include changes between unrelated A and B
749 748 # plus changes induced by rebase)
750 749 # Which does not represent anything sensible and creates a lot of
751 750 # conflicts. A is thus not the right choice - B is.
752 751 #
753 752 # Note: The base found in this 'proof' is only correct in the specified
754 753 # case. This base does not make sense if is not D a descendant of A or B
755 754 # or if the other is not parent 'outside' (especially not if the other
756 755 # parent has been rebased). The current implementation does not
757 756 # make it feasible to consider different cases separately. In these
758 757 # other cases we currently just leave it to the user to correctly
759 758 # resolve an impossible merge using a wrong ancestor.
760 759 for p in repo[rev].parents():
761 760 if state.get(p.rev()) == p1:
762 761 base = p.rev()
763 762 break
764 763 else: # fallback when base not found
765 764 base = None
766 765
767 766 # Raise because this function is called wrong (see issue 4106)
768 767 raise AssertionError('no base found to rebase on '
769 768 '(defineparents called wrong)')
770 769 return p1, p2, base
771 770
772 771 def isagitpatch(repo, patchname):
773 772 'Return true if the given patch is in git format'
774 773 mqpatch = os.path.join(repo.mq.path, patchname)
775 774 for line in patch.linereader(file(mqpatch, 'rb')):
776 775 if line.startswith('diff --git'):
777 776 return True
778 777 return False
779 778
780 779 def updatemq(repo, state, skipped, **opts):
781 780 'Update rebased mq patches - finalize and then import them'
782 781 mqrebase = {}
783 782 mq = repo.mq
784 783 original_series = mq.fullseries[:]
785 784 skippedpatches = set()
786 785
787 786 for p in mq.applied:
788 787 rev = repo[p.node].rev()
789 788 if rev in state:
790 789 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
791 790 (rev, p.name))
792 791 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
793 792 else:
794 793 # Applied but not rebased, not sure this should happen
795 794 skippedpatches.add(p.name)
796 795
797 796 if mqrebase:
798 797 mq.finish(repo, mqrebase.keys())
799 798
800 799 # We must start import from the newest revision
801 800 for rev in sorted(mqrebase, reverse=True):
802 801 if rev not in skipped:
803 802 name, isgit = mqrebase[rev]
804 803 repo.ui.note(_('updating mq patch %s to %s:%s\n') %
805 804 (name, state[rev], repo[state[rev]]))
806 805 mq.qimport(repo, (), patchname=name, git=isgit,
807 806 rev=[str(state[rev])])
808 807 else:
809 808 # Rebased and skipped
810 809 skippedpatches.add(mqrebase[rev][0])
811 810
812 811 # Patches were either applied and rebased and imported in
813 812 # order, applied and removed or unapplied. Discard the removed
814 813 # ones while preserving the original series order and guards.
815 814 newseries = [s for s in original_series
816 815 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
817 816 mq.fullseries[:] = newseries
818 817 mq.seriesdirty = True
819 818 mq.savedirty()
820 819
821 820 def updatebookmarks(repo, targetnode, nstate, originalbookmarks, tr):
822 821 'Move bookmarks to their correct changesets, and delete divergent ones'
823 822 marks = repo._bookmarks
824 823 for k, v in originalbookmarks.iteritems():
825 824 if v in nstate:
826 825 # update the bookmarks for revs that have moved
827 826 marks[k] = nstate[v]
828 827 bookmarks.deletedivergent(repo, [targetnode], k)
829 828 marks.recordchange(tr)
830 829
831 830 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
832 831 external, activebookmark):
833 832 'Store the current status to allow recovery'
834 833 f = repo.vfs("rebasestate", "w")
835 834 f.write(repo[originalwd].hex() + '\n')
836 835 f.write(repo[target].hex() + '\n')
837 836 f.write(repo[external].hex() + '\n')
838 837 f.write('%d\n' % int(collapse))
839 838 f.write('%d\n' % int(keep))
840 839 f.write('%d\n' % int(keepbranches))
841 840 f.write('%s\n' % (activebookmark or ''))
842 841 for d, v in state.iteritems():
843 842 oldrev = repo[d].hex()
844 843 if v >= 0:
845 844 newrev = repo[v].hex()
846 845 elif v == revtodo:
847 846 # To maintain format compatibility, we have to use nullid.
848 847 # Please do remove this special case when upgrading the format.
849 848 newrev = hex(nullid)
850 849 else:
851 850 newrev = v
852 851 f.write("%s:%s\n" % (oldrev, newrev))
853 852 f.close()
854 853 repo.ui.debug('rebase status stored\n')
855 854
856 855 def clearstatus(repo):
857 856 'Remove the status files'
858 857 _clearrebasesetvisibiliy(repo)
859 858 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
860 859
861 860 def restorestatus(repo):
862 861 'Restore a previously stored status'
863 862 keepbranches = None
864 863 target = None
865 864 collapse = False
866 865 external = nullrev
867 866 activebookmark = None
868 867 state = {}
869 868
870 869 try:
871 870 f = repo.vfs("rebasestate")
872 871 for i, l in enumerate(f.read().splitlines()):
873 872 if i == 0:
874 873 originalwd = repo[l].rev()
875 874 elif i == 1:
876 875 target = repo[l].rev()
877 876 elif i == 2:
878 877 external = repo[l].rev()
879 878 elif i == 3:
880 879 collapse = bool(int(l))
881 880 elif i == 4:
882 881 keep = bool(int(l))
883 882 elif i == 5:
884 883 keepbranches = bool(int(l))
885 884 elif i == 6 and not (len(l) == 81 and ':' in l):
886 885 # line 6 is a recent addition, so for backwards compatibility
887 886 # check that the line doesn't look like the oldrev:newrev lines
888 887 activebookmark = l
889 888 else:
890 889 oldrev, newrev = l.split(':')
891 890 if newrev in (str(nullmerge), str(revignored),
892 891 str(revprecursor), str(revpruned)):
893 892 state[repo[oldrev].rev()] = int(newrev)
894 893 elif newrev == nullid:
895 894 state[repo[oldrev].rev()] = revtodo
896 895 # Legacy compat special case
897 896 else:
898 897 state[repo[oldrev].rev()] = repo[newrev].rev()
899 898
900 899 except IOError as err:
901 900 if err.errno != errno.ENOENT:
902 901 raise
903 902 raise error.Abort(_('no rebase in progress'))
904 903
905 904 if keepbranches is None:
906 905 raise error.Abort(_('.hg/rebasestate is incomplete'))
907 906
908 907 skipped = set()
909 908 # recompute the set of skipped revs
910 909 if not collapse:
911 910 seen = set([target])
912 911 for old, new in sorted(state.items()):
913 912 if new != revtodo and new in seen:
914 913 skipped.add(old)
915 914 seen.add(new)
916 915 repo.ui.debug('computed skipped revs: %s\n' %
917 916 (' '.join(str(r) for r in sorted(skipped)) or None))
918 917 repo.ui.debug('rebase status resumed\n')
919 918 _setrebasesetvisibility(repo, state.keys())
920 919 return (originalwd, target, state, skipped,
921 920 collapse, keep, keepbranches, external, activebookmark)
922 921
923 922 def needupdate(repo, state):
924 923 '''check whether we should `update --clean` away from a merge, or if
925 924 somehow the working dir got forcibly updated, e.g. by older hg'''
926 925 parents = [p.rev() for p in repo[None].parents()]
927 926
928 927 # Are we in a merge state at all?
929 928 if len(parents) < 2:
930 929 return False
931 930
932 931 # We should be standing on the first as-of-yet unrebased commit.
933 932 firstunrebased = min([old for old, new in state.iteritems()
934 933 if new == nullrev])
935 934 if firstunrebased in parents:
936 935 return True
937 936
938 937 return False
939 938
940 939 def abort(repo, originalwd, target, state, activebookmark=None):
941 940 '''Restore the repository to its original state. Additional args:
942 941
943 942 activebookmark: the name of the bookmark that should be active after the
944 943 restore'''
945 944
946 945 try:
947 946 # If the first commits in the rebased set get skipped during the rebase,
948 947 # their values within the state mapping will be the target rev id. The
949 948 # dstates list must must not contain the target rev (issue4896)
950 949 dstates = [s for s in state.values() if s >= 0 and s != target]
951 950 immutable = [d for d in dstates if not repo[d].mutable()]
952 951 cleanup = True
953 952 if immutable:
954 953 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
955 954 % ', '.join(str(repo[r]) for r in immutable),
956 955 hint=_('see "hg help phases" for details'))
957 956 cleanup = False
958 957
959 958 descendants = set()
960 959 if dstates:
961 960 descendants = set(repo.changelog.descendants(dstates))
962 961 if descendants - set(dstates):
963 962 repo.ui.warn(_("warning: new changesets detected on target branch, "
964 963 "can't strip\n"))
965 964 cleanup = False
966 965
967 966 if cleanup:
968 967 # Update away from the rebase if necessary
969 968 if needupdate(repo, state):
970 969 merge.update(repo, originalwd, False, True)
971 970
972 971 # Strip from the first rebased revision
973 972 rebased = filter(lambda x: x >= 0 and x != target, state.values())
974 973 if rebased:
975 974 strippoints = [
976 975 c.node() for c in repo.set('roots(%ld)', rebased)]
977 976 # no backup of rebased cset versions needed
978 977 repair.strip(repo.ui, repo, strippoints)
979 978
980 979 if activebookmark and activebookmark in repo._bookmarks:
981 980 bookmarks.activate(repo, activebookmark)
982 981
983 982 finally:
984 983 clearstatus(repo)
985 984 repo.ui.warn(_('rebase aborted\n'))
986 985 return 0
987 986
988 987 def buildstate(repo, dest, rebaseset, collapse, obsoletenotrebased):
989 988 '''Define which revisions are going to be rebased and where
990 989
991 990 repo: repo
992 991 dest: context
993 992 rebaseset: set of rev
994 993 '''
995 994 _setrebasesetvisibility(repo, rebaseset)
996 995
997 996 # This check isn't strictly necessary, since mq detects commits over an
998 997 # applied patch. But it prevents messing up the working directory when
999 998 # a partially completed rebase is blocked by mq.
1000 999 if 'qtip' in repo.tags() and (dest.node() in
1001 1000 [s.node for s in repo.mq.applied]):
1002 1001 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1003 1002
1004 1003 roots = list(repo.set('roots(%ld)', rebaseset))
1005 1004 if not roots:
1006 1005 raise error.Abort(_('no matching revisions'))
1007 1006 roots.sort()
1008 1007 state = {}
1009 1008 detachset = set()
1010 1009 for root in roots:
1011 1010 commonbase = root.ancestor(dest)
1012 1011 if commonbase == root:
1013 1012 raise error.Abort(_('source is ancestor of destination'))
1014 1013 if commonbase == dest:
1015 1014 samebranch = root.branch() == dest.branch()
1016 1015 if not collapse and samebranch and root in dest.children():
1017 1016 repo.ui.debug('source is a child of destination\n')
1018 1017 return None
1019 1018
1020 1019 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, root))
1021 1020 state.update(dict.fromkeys(rebaseset, revtodo))
1022 1021 # Rebase tries to turn <dest> into a parent of <root> while
1023 1022 # preserving the number of parents of rebased changesets:
1024 1023 #
1025 1024 # - A changeset with a single parent will always be rebased as a
1026 1025 # changeset with a single parent.
1027 1026 #
1028 1027 # - A merge will be rebased as merge unless its parents are both
1029 1028 # ancestors of <dest> or are themselves in the rebased set and
1030 1029 # pruned while rebased.
1031 1030 #
1032 1031 # If one parent of <root> is an ancestor of <dest>, the rebased
1033 1032 # version of this parent will be <dest>. This is always true with
1034 1033 # --base option.
1035 1034 #
1036 1035 # Otherwise, we need to *replace* the original parents with
1037 1036 # <dest>. This "detaches" the rebased set from its former location
1038 1037 # and rebases it onto <dest>. Changes introduced by ancestors of
1039 1038 # <root> not common with <dest> (the detachset, marked as
1040 1039 # nullmerge) are "removed" from the rebased changesets.
1041 1040 #
1042 1041 # - If <root> has a single parent, set it to <dest>.
1043 1042 #
1044 1043 # - If <root> is a merge, we cannot decide which parent to
1045 1044 # replace, the rebase operation is not clearly defined.
1046 1045 #
1047 1046 # The table below sums up this behavior:
1048 1047 #
1049 1048 # +------------------+----------------------+-------------------------+
1050 1049 # | | one parent | merge |
1051 1050 # +------------------+----------------------+-------------------------+
1052 1051 # | parent in | new parent is <dest> | parents in ::<dest> are |
1053 1052 # | ::<dest> | | remapped to <dest> |
1054 1053 # +------------------+----------------------+-------------------------+
1055 1054 # | unrelated source | new parent is <dest> | ambiguous, abort |
1056 1055 # +------------------+----------------------+-------------------------+
1057 1056 #
1058 1057 # The actual abort is handled by `defineparents`
1059 1058 if len(root.parents()) <= 1:
1060 1059 # ancestors of <root> not ancestors of <dest>
1061 1060 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
1062 1061 [root.rev()]))
1063 1062 for r in detachset:
1064 1063 if r not in state:
1065 1064 state[r] = nullmerge
1066 1065 if len(roots) > 1:
1067 1066 # If we have multiple roots, we may have "hole" in the rebase set.
1068 1067 # Rebase roots that descend from those "hole" should not be detached as
1069 1068 # other root are. We use the special `revignored` to inform rebase that
1070 1069 # the revision should be ignored but that `defineparents` should search
1071 1070 # a rebase destination that make sense regarding rebased topology.
1072 1071 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
1073 1072 for ignored in set(rebasedomain) - set(rebaseset):
1074 1073 state[ignored] = revignored
1075 1074 for r in obsoletenotrebased:
1076 1075 if obsoletenotrebased[r] is None:
1077 1076 state[r] = revpruned
1078 1077 else:
1079 1078 state[r] = revprecursor
1080 1079 return repo['.'].rev(), dest.rev(), state
1081 1080
1082 1081 def clearrebased(ui, repo, state, skipped, collapsedas=None):
1083 1082 """dispose of rebased revision at the end of the rebase
1084 1083
1085 1084 If `collapsedas` is not None, the rebase was a collapse whose result if the
1086 1085 `collapsedas` node."""
1087 1086 if obsolete.isenabled(repo, obsolete.createmarkersopt):
1088 1087 markers = []
1089 1088 for rev, newrev in sorted(state.items()):
1090 1089 if newrev >= 0:
1091 1090 if rev in skipped:
1092 1091 succs = ()
1093 1092 elif collapsedas is not None:
1094 1093 succs = (repo[collapsedas],)
1095 1094 else:
1096 1095 succs = (repo[newrev],)
1097 1096 markers.append((repo[rev], succs))
1098 1097 if markers:
1099 1098 obsolete.createmarkers(repo, markers)
1100 1099 else:
1101 1100 rebased = [rev for rev in state if state[rev] > nullmerge]
1102 1101 if rebased:
1103 1102 stripped = []
1104 1103 for root in repo.set('roots(%ld)', rebased):
1105 1104 if set(repo.changelog.descendants([root.rev()])) - set(state):
1106 1105 ui.warn(_("warning: new changesets detected "
1107 1106 "on source branch, not stripping\n"))
1108 1107 else:
1109 1108 stripped.append(root.node())
1110 1109 if stripped:
1111 1110 # backup the old csets by default
1112 1111 repair.strip(ui, repo, stripped, "all")
1113 1112
1114 1113
1115 1114 def pullrebase(orig, ui, repo, *args, **opts):
1116 1115 'Call rebase after pull if the latter has been invoked with --rebase'
1117 1116 ret = None
1118 1117 if opts.get('rebase'):
1119 1118 wlock = lock = None
1120 1119 try:
1121 1120 wlock = repo.wlock()
1122 1121 lock = repo.lock()
1123 1122 if opts.get('update'):
1124 1123 del opts['update']
1125 1124 ui.debug('--update and --rebase are not compatible, ignoring '
1126 1125 'the update flag\n')
1127 1126
1128 1127 movemarkfrom = repo['.'].node()
1129 1128 revsprepull = len(repo)
1130 1129 origpostincoming = commands.postincoming
1131 1130 def _dummy(*args, **kwargs):
1132 1131 pass
1133 1132 commands.postincoming = _dummy
1134 1133 try:
1135 1134 ret = orig(ui, repo, *args, **opts)
1136 1135 finally:
1137 1136 commands.postincoming = origpostincoming
1138 1137 revspostpull = len(repo)
1139 1138 if revspostpull > revsprepull:
1140 1139 # --rev option from pull conflict with rebase own --rev
1141 1140 # dropping it
1142 1141 if 'rev' in opts:
1143 1142 del opts['rev']
1144 1143 # positional argument from pull conflicts with rebase's own
1145 1144 # --source.
1146 1145 if 'source' in opts:
1147 1146 del opts['source']
1148 1147 rebase(ui, repo, **opts)
1149 1148 branch = repo[None].branch()
1150 1149 dest = repo[branch].rev()
1151 1150 if dest != repo['.'].rev():
1152 1151 # there was nothing to rebase we force an update
1153 1152 hg.update(repo, dest)
1154 1153 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
1155 1154 ui.status(_("updating bookmark %s\n")
1156 1155 % repo._activebookmark)
1157 1156 finally:
1158 1157 release(lock, wlock)
1159 1158 else:
1160 1159 if opts.get('tool'):
1161 1160 raise error.Abort(_('--tool can only be used with --rebase'))
1162 1161 ret = orig(ui, repo, *args, **opts)
1163 1162
1164 1163 return ret
1165 1164
1166 1165 def _setrebasesetvisibility(repo, revs):
1167 1166 """store the currently rebased set on the repo object
1168 1167
1169 1168 This is used by another function to prevent rebased revision to because
1170 1169 hidden (see issue4505)"""
1171 1170 repo = repo.unfiltered()
1172 1171 revs = set(revs)
1173 1172 repo._rebaseset = revs
1174 1173 # invalidate cache if visibility changes
1175 1174 hiddens = repo.filteredrevcache.get('visible', set())
1176 1175 if revs & hiddens:
1177 1176 repo.invalidatevolatilesets()
1178 1177
1179 1178 def _clearrebasesetvisibiliy(repo):
1180 1179 """remove rebaseset data from the repo"""
1181 1180 repo = repo.unfiltered()
1182 1181 if '_rebaseset' in vars(repo):
1183 1182 del repo._rebaseset
1184 1183
1185 1184 def _rebasedvisible(orig, repo):
1186 1185 """ensure rebased revs stay visible (see issue4505)"""
1187 1186 blockers = orig(repo)
1188 1187 blockers.update(getattr(repo, '_rebaseset', ()))
1189 1188 return blockers
1190 1189
1190 def _filterobsoleterevs(repo, revs):
1191 """returns a set of the obsolete revisions in revs"""
1192 return set(r for r in revs if repo[r].obsolete())
1193
1191 1194 def _computeobsoletenotrebased(repo, rebaseobsrevs, dest):
1192 1195 """return a mapping obsolete => successor for all obsolete nodes to be
1193 1196 rebased that have a successors in the destination
1194 1197
1195 1198 obsolete => None entries in the mapping indicate nodes with no succesor"""
1196 1199 obsoletenotrebased = {}
1197 1200
1198 1201 # Build a mapping successor => obsolete nodes for the obsolete
1199 1202 # nodes to be rebased
1200 1203 allsuccessors = {}
1201 1204 cl = repo.changelog
1202 1205 for r in rebaseobsrevs:
1203 1206 node = cl.node(r)
1204 1207 for s in obsolete.allsuccessors(repo.obsstore, [node]):
1205 1208 try:
1206 1209 allsuccessors[cl.rev(s)] = cl.rev(node)
1207 1210 except LookupError:
1208 1211 pass
1209 1212
1210 1213 if allsuccessors:
1211 1214 # Look for successors of obsolete nodes to be rebased among
1212 1215 # the ancestors of dest
1213 1216 ancs = cl.ancestors([repo[dest].rev()],
1214 1217 stoprev=min(allsuccessors),
1215 1218 inclusive=True)
1216 1219 for s in allsuccessors:
1217 1220 if s in ancs:
1218 1221 obsoletenotrebased[allsuccessors[s]] = s
1219 1222 elif (s == allsuccessors[s] and
1220 1223 allsuccessors.values().count(s) == 1):
1221 1224 # plain prune
1222 1225 obsoletenotrebased[s] = None
1223 1226
1224 1227 return obsoletenotrebased
1225 1228
1226 1229 def summaryhook(ui, repo):
1227 1230 if not os.path.exists(repo.join('rebasestate')):
1228 1231 return
1229 1232 try:
1230 1233 state = restorestatus(repo)[2]
1231 1234 except error.RepoLookupError:
1232 1235 # i18n: column positioning for "hg summary"
1233 1236 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1234 1237 ui.write(msg)
1235 1238 return
1236 1239 numrebased = len([i for i in state.itervalues() if i >= 0])
1237 1240 # i18n: column positioning for "hg summary"
1238 1241 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1239 1242 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1240 1243 ui.label(_('%d remaining'), 'rebase.remaining') %
1241 1244 (len(state) - numrebased)))
1242 1245
1243 1246 def uisetup(ui):
1244 1247 #Replace pull with a decorator to provide --rebase option
1245 1248 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1246 1249 entry[1].append(('', 'rebase', None,
1247 1250 _("rebase working directory to branch head")))
1248 1251 entry[1].append(('t', 'tool', '',
1249 1252 _("specify merge tool for rebase")))
1250 1253 cmdutil.summaryhooks.add('rebase', summaryhook)
1251 1254 cmdutil.unfinishedstates.append(
1252 1255 ['rebasestate', False, False, _('rebase in progress'),
1253 1256 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
1254 1257 cmdutil.afterresolvedstates.append(
1255 1258 ['rebasestate', _('hg rebase --continue')])
1256 1259 # ensure rebased rev are not hidden
1257 1260 extensions.wrapfunction(repoview, '_getdynamicblockers', _rebasedvisible)
1258 1261 revsetpredicate.setup()
General Comments 0
You need to be logged in to leave comments. Login now