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