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