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