##// END OF EJS Templates
rebase: do not bail on uncomitted changes if rebasing in-memory...
Phil Cohen -
r35292:aa660c12 default
parent child Browse files
Show More
@@ -1,1728 +1,1730 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 nullid,
25 25 nullrev,
26 26 short,
27 27 )
28 28 from mercurial import (
29 29 bookmarks,
30 30 cmdutil,
31 31 commands,
32 32 copies,
33 33 destutil,
34 34 dirstateguard,
35 35 error,
36 36 extensions,
37 37 hg,
38 38 lock,
39 39 merge as mergemod,
40 40 mergeutil,
41 41 obsolete,
42 42 obsutil,
43 43 patch,
44 44 phases,
45 45 pycompat,
46 46 registrar,
47 47 repair,
48 48 revset,
49 49 revsetlang,
50 50 scmutil,
51 51 smartset,
52 52 util,
53 53 )
54 54
55 55 release = lock.release
56 56
57 57 # The following constants are used throughout the rebase module. The ordering of
58 58 # their values must be maintained.
59 59
60 60 # Indicates that a revision needs to be rebased
61 61 revtodo = -1
62 62 revtodostr = '-1'
63 63
64 64 # legacy revstates no longer needed in current code
65 65 # -2: nullmerge, -3: revignored, -4: revprecursor, -5: revpruned
66 66 legacystates = {'-2', '-3', '-4', '-5'}
67 67
68 68 cmdtable = {}
69 69 command = registrar.command(cmdtable)
70 70 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
71 71 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
72 72 # be specifying the version(s) of Mercurial they are tested with, or
73 73 # leave the attribute unspecified.
74 74 testedwith = 'ships-with-hg-core'
75 75
76 76 def _nothingtorebase():
77 77 return 1
78 78
79 79 def _savegraft(ctx, extra):
80 80 s = ctx.extra().get('source', None)
81 81 if s is not None:
82 82 extra['source'] = s
83 83 s = ctx.extra().get('intermediate-source', None)
84 84 if s is not None:
85 85 extra['intermediate-source'] = s
86 86
87 87 def _savebranch(ctx, extra):
88 88 extra['branch'] = ctx.branch()
89 89
90 90 def _makeextrafn(copiers):
91 91 """make an extrafn out of the given copy-functions.
92 92
93 93 A copy function takes a context and an extra dict, and mutates the
94 94 extra dict as needed based on the given context.
95 95 """
96 96 def extrafn(ctx, extra):
97 97 for c in copiers:
98 98 c(ctx, extra)
99 99 return extrafn
100 100
101 101 def _destrebase(repo, sourceset, destspace=None):
102 102 """small wrapper around destmerge to pass the right extra args
103 103
104 104 Please wrap destutil.destmerge instead."""
105 105 return destutil.destmerge(repo, action='rebase', sourceset=sourceset,
106 106 onheadcheck=False, destspace=destspace)
107 107
108 108 revsetpredicate = registrar.revsetpredicate()
109 109
110 110 @revsetpredicate('_destrebase')
111 111 def _revsetdestrebase(repo, subset, x):
112 112 # ``_rebasedefaultdest()``
113 113
114 114 # default destination for rebase.
115 115 # # XXX: Currently private because I expect the signature to change.
116 116 # # XXX: - bailing out in case of ambiguity vs returning all data.
117 117 # i18n: "_rebasedefaultdest" is a keyword
118 118 sourceset = None
119 119 if x is not None:
120 120 sourceset = revset.getset(repo, smartset.fullreposet(repo), x)
121 121 return subset & smartset.baseset([_destrebase(repo, sourceset)])
122 122
123 123 def _ctxdesc(ctx):
124 124 """short description for a context"""
125 125 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
126 126 ctx.description().split('\n', 1)[0])
127 127 repo = ctx.repo()
128 128 names = []
129 129 for nsname, ns in repo.names.iteritems():
130 130 if nsname == 'branches':
131 131 continue
132 132 names.extend(ns.names(repo, ctx.node()))
133 133 if names:
134 134 desc += ' (%s)' % ' '.join(names)
135 135 return desc
136 136
137 137 class rebaseruntime(object):
138 138 """This class is a container for rebase runtime state"""
139 139 def __init__(self, repo, ui, opts=None):
140 140 if opts is None:
141 141 opts = {}
142 142
143 143 # prepared: whether we have rebasestate prepared or not. Currently it
144 144 # decides whether "self.repo" is unfiltered or not.
145 145 # The rebasestate has explicit hash to hash instructions not depending
146 146 # on visibility. If rebasestate exists (in-memory or on-disk), use
147 147 # unfiltered repo to avoid visibility issues.
148 148 # Before knowing rebasestate (i.e. when starting a new rebase (not
149 149 # --continue or --abort)), the original repo should be used so
150 150 # visibility-dependent revsets are correct.
151 151 self.prepared = False
152 152 self._repo = repo
153 153
154 154 self.ui = ui
155 155 self.opts = opts
156 156 self.originalwd = None
157 157 self.external = nullrev
158 158 # Mapping between the old revision id and either what is the new rebased
159 159 # revision or what needs to be done with the old revision. The state
160 160 # dict will be what contains most of the rebase progress state.
161 161 self.state = {}
162 162 self.activebookmark = None
163 163 self.destmap = {}
164 164 self.skipped = set()
165 165
166 166 self.collapsef = opts.get('collapse', False)
167 167 self.collapsemsg = cmdutil.logmessage(ui, opts)
168 168 self.date = opts.get('date', None)
169 169
170 170 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
171 171 self.extrafns = [_savegraft]
172 172 if e:
173 173 self.extrafns = [e]
174 174
175 175 self.keepf = opts.get('keep', False)
176 176 self.keepbranchesf = opts.get('keepbranches', False)
177 177 # keepopen is not meant for use on the command line, but by
178 178 # other extensions
179 179 self.keepopen = opts.get('keepopen', False)
180 180 self.obsoletenotrebased = {}
181 181 self.obsoletewithoutsuccessorindestination = set()
182 182 self.inmemory = opts.get('inmemory', False)
183 183
184 184 @property
185 185 def repo(self):
186 186 if self.prepared:
187 187 return self._repo.unfiltered()
188 188 else:
189 189 return self._repo
190 190
191 191 def storestatus(self, tr=None):
192 192 """Store the current status to allow recovery"""
193 193 if tr:
194 194 tr.addfilegenerator('rebasestate', ('rebasestate',),
195 195 self._writestatus, location='plain')
196 196 else:
197 197 with self.repo.vfs("rebasestate", "w") as f:
198 198 self._writestatus(f)
199 199
200 200 def _writestatus(self, f):
201 201 repo = self.repo
202 202 assert repo.filtername is None
203 203 f.write(repo[self.originalwd].hex() + '\n')
204 204 # was "dest". we now write dest per src root below.
205 205 f.write('\n')
206 206 f.write(repo[self.external].hex() + '\n')
207 207 f.write('%d\n' % int(self.collapsef))
208 208 f.write('%d\n' % int(self.keepf))
209 209 f.write('%d\n' % int(self.keepbranchesf))
210 210 f.write('%s\n' % (self.activebookmark or ''))
211 211 destmap = self.destmap
212 212 for d, v in self.state.iteritems():
213 213 oldrev = repo[d].hex()
214 214 if v >= 0:
215 215 newrev = repo[v].hex()
216 216 else:
217 217 newrev = v
218 218 destnode = repo[destmap[d]].hex()
219 219 f.write("%s:%s:%s\n" % (oldrev, newrev, destnode))
220 220 repo.ui.debug('rebase status stored\n')
221 221
222 222 def restorestatus(self):
223 223 """Restore a previously stored status"""
224 224 self.prepared = True
225 225 repo = self.repo
226 226 assert repo.filtername is None
227 227 keepbranches = None
228 228 legacydest = None
229 229 collapse = False
230 230 external = nullrev
231 231 activebookmark = None
232 232 state = {}
233 233 destmap = {}
234 234
235 235 try:
236 236 f = repo.vfs("rebasestate")
237 237 for i, l in enumerate(f.read().splitlines()):
238 238 if i == 0:
239 239 originalwd = repo[l].rev()
240 240 elif i == 1:
241 241 # this line should be empty in newer version. but legacy
242 242 # clients may still use it
243 243 if l:
244 244 legacydest = repo[l].rev()
245 245 elif i == 2:
246 246 external = repo[l].rev()
247 247 elif i == 3:
248 248 collapse = bool(int(l))
249 249 elif i == 4:
250 250 keep = bool(int(l))
251 251 elif i == 5:
252 252 keepbranches = bool(int(l))
253 253 elif i == 6 and not (len(l) == 81 and ':' in l):
254 254 # line 6 is a recent addition, so for backwards
255 255 # compatibility check that the line doesn't look like the
256 256 # oldrev:newrev lines
257 257 activebookmark = l
258 258 else:
259 259 args = l.split(':')
260 260 oldrev = args[0]
261 261 newrev = args[1]
262 262 if newrev in legacystates:
263 263 continue
264 264 if len(args) > 2:
265 265 destnode = args[2]
266 266 else:
267 267 destnode = legacydest
268 268 destmap[repo[oldrev].rev()] = repo[destnode].rev()
269 269 if newrev in (nullid, revtodostr):
270 270 state[repo[oldrev].rev()] = revtodo
271 271 # Legacy compat special case
272 272 else:
273 273 state[repo[oldrev].rev()] = repo[newrev].rev()
274 274
275 275 except IOError as err:
276 276 if err.errno != errno.ENOENT:
277 277 raise
278 278 cmdutil.wrongtooltocontinue(repo, _('rebase'))
279 279
280 280 if keepbranches is None:
281 281 raise error.Abort(_('.hg/rebasestate is incomplete'))
282 282
283 283 skipped = set()
284 284 # recompute the set of skipped revs
285 285 if not collapse:
286 286 seen = set(destmap.values())
287 287 for old, new in sorted(state.items()):
288 288 if new != revtodo and new in seen:
289 289 skipped.add(old)
290 290 seen.add(new)
291 291 repo.ui.debug('computed skipped revs: %s\n' %
292 292 (' '.join(str(r) for r in sorted(skipped)) or None))
293 293 repo.ui.debug('rebase status resumed\n')
294 294
295 295 self.originalwd = originalwd
296 296 self.destmap = destmap
297 297 self.state = state
298 298 self.skipped = skipped
299 299 self.collapsef = collapse
300 300 self.keepf = keep
301 301 self.keepbranchesf = keepbranches
302 302 self.external = external
303 303 self.activebookmark = activebookmark
304 304
305 305 def _handleskippingobsolete(self, obsoleterevs, destmap):
306 306 """Compute structures necessary for skipping obsolete revisions
307 307
308 308 obsoleterevs: iterable of all obsolete revisions in rebaseset
309 309 destmap: {srcrev: destrev} destination revisions
310 310 """
311 311 self.obsoletenotrebased = {}
312 312 if not self.ui.configbool('experimental', 'rebaseskipobsolete'):
313 313 return
314 314 obsoleteset = set(obsoleterevs)
315 315 self.obsoletenotrebased, self.obsoletewithoutsuccessorindestination = \
316 316 _computeobsoletenotrebased(self.repo, obsoleteset, destmap)
317 317 skippedset = set(self.obsoletenotrebased)
318 318 skippedset.update(self.obsoletewithoutsuccessorindestination)
319 319 _checkobsrebase(self.repo, self.ui, obsoleteset, skippedset)
320 320
321 321 def _prepareabortorcontinue(self, isabort):
322 322 try:
323 323 self.restorestatus()
324 324 self.collapsemsg = restorecollapsemsg(self.repo, isabort)
325 325 except error.RepoLookupError:
326 326 if isabort:
327 327 clearstatus(self.repo)
328 328 clearcollapsemsg(self.repo)
329 329 self.repo.ui.warn(_('rebase aborted (no revision is removed,'
330 330 ' only broken state is cleared)\n'))
331 331 return 0
332 332 else:
333 333 msg = _('cannot continue inconsistent rebase')
334 334 hint = _('use "hg rebase --abort" to clear broken state')
335 335 raise error.Abort(msg, hint=hint)
336 336 if isabort:
337 337 return abort(self.repo, self.originalwd, self.destmap,
338 338 self.state, activebookmark=self.activebookmark)
339 339
340 340 def _preparenewrebase(self, destmap):
341 341 if not destmap:
342 342 return _nothingtorebase()
343 343
344 344 rebaseset = destmap.keys()
345 345 allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
346 346 if (not (self.keepf or allowunstable)
347 347 and self.repo.revs('first(children(%ld) - %ld)',
348 348 rebaseset, rebaseset)):
349 349 raise error.Abort(
350 350 _("can't remove original changesets with"
351 351 " unrebased descendants"),
352 352 hint=_('use --keep to keep original changesets'))
353 353
354 354 result = buildstate(self.repo, destmap, self.collapsef)
355 355
356 356 if not result:
357 357 # Empty state built, nothing to rebase
358 358 self.ui.status(_('nothing to rebase\n'))
359 359 return _nothingtorebase()
360 360
361 361 for root in self.repo.set('roots(%ld)', rebaseset):
362 362 if not self.keepf and not root.mutable():
363 363 raise error.Abort(_("can't rebase public changeset %s")
364 364 % root,
365 365 hint=_("see 'hg help phases' for details"))
366 366
367 367 (self.originalwd, self.destmap, self.state) = result
368 368 if self.collapsef:
369 369 dests = set(self.destmap.values())
370 370 if len(dests) != 1:
371 371 raise error.Abort(
372 372 _('--collapse does not work with multiple destinations'))
373 373 destrev = next(iter(dests))
374 374 destancestors = self.repo.changelog.ancestors([destrev],
375 375 inclusive=True)
376 376 self.external = externalparent(self.repo, self.state, destancestors)
377 377
378 378 for destrev in sorted(set(destmap.values())):
379 379 dest = self.repo[destrev]
380 380 if dest.closesbranch() and not self.keepbranchesf:
381 381 self.ui.status(_('reopening closed branch head %s\n') % dest)
382 382
383 383 self.prepared = True
384 384
385 385 def _performrebase(self, tr):
386 386 repo, ui = self.repo, self.ui
387 387 # Assign a working copy object.
388 388 if self.inmemory:
389 389 from mercurial.context import overlayworkingctx
390 390 self.wctx = overlayworkingctx(self.repo)
391 391 else:
392 392 self.wctx = self.repo[None]
393 393 if self.keepbranchesf:
394 394 # insert _savebranch at the start of extrafns so if
395 395 # there's a user-provided extrafn it can clobber branch if
396 396 # desired
397 397 self.extrafns.insert(0, _savebranch)
398 398 if self.collapsef:
399 399 branches = set()
400 400 for rev in self.state:
401 401 branches.add(repo[rev].branch())
402 402 if len(branches) > 1:
403 403 raise error.Abort(_('cannot collapse multiple named '
404 404 'branches'))
405 405
406 406 # Calculate self.obsoletenotrebased
407 407 obsrevs = _filterobsoleterevs(self.repo, self.state)
408 408 self._handleskippingobsolete(obsrevs, self.destmap)
409 409
410 410 # Keep track of the active bookmarks in order to reset them later
411 411 self.activebookmark = self.activebookmark or repo._activebookmark
412 412 if self.activebookmark:
413 413 bookmarks.deactivate(repo)
414 414
415 415 # Store the state before we begin so users can run 'hg rebase --abort'
416 416 # if we fail before the transaction closes.
417 417 self.storestatus()
418 418
419 419 cands = [k for k, v in self.state.iteritems() if v == revtodo]
420 420 total = len(cands)
421 421 pos = 0
422 422 for subset in sortsource(self.destmap):
423 423 pos = self._performrebasesubset(tr, subset, pos, total)
424 424 ui.progress(_('rebasing'), None)
425 425 ui.note(_('rebase merging completed\n'))
426 426
427 427 def _performrebasesubset(self, tr, subset, pos, total):
428 428 repo, ui, opts = self.repo, self.ui, self.opts
429 429 sortedrevs = repo.revs('sort(%ld, -topo)', subset)
430 430 allowdivergence = self.ui.configbool(
431 431 'experimental', 'evolution.allowdivergence')
432 432 if not allowdivergence:
433 433 sortedrevs -= repo.revs(
434 434 'descendants(%ld) and not %ld',
435 435 self.obsoletewithoutsuccessorindestination,
436 436 self.obsoletewithoutsuccessorindestination,
437 437 )
438 438 for rev in sortedrevs:
439 439 dest = self.destmap[rev]
440 440 ctx = repo[rev]
441 441 desc = _ctxdesc(ctx)
442 442 if self.state[rev] == rev:
443 443 ui.status(_('already rebased %s\n') % desc)
444 444 elif (not allowdivergence
445 445 and rev in self.obsoletewithoutsuccessorindestination):
446 446 msg = _('note: not rebasing %s and its descendants as '
447 447 'this would cause divergence\n') % desc
448 448 repo.ui.status(msg)
449 449 self.skipped.add(rev)
450 450 elif rev in self.obsoletenotrebased:
451 451 succ = self.obsoletenotrebased[rev]
452 452 if succ is None:
453 453 msg = _('note: not rebasing %s, it has no '
454 454 'successor\n') % desc
455 455 else:
456 456 succdesc = _ctxdesc(repo[succ])
457 457 msg = (_('note: not rebasing %s, already in '
458 458 'destination as %s\n') % (desc, succdesc))
459 459 repo.ui.status(msg)
460 460 # Make clearrebased aware state[rev] is not a true successor
461 461 self.skipped.add(rev)
462 462 # Record rev as moved to its desired destination in self.state.
463 463 # This helps bookmark and working parent movement.
464 464 dest = max(adjustdest(repo, rev, self.destmap, self.state,
465 465 self.skipped))
466 466 self.state[rev] = dest
467 467 elif self.state[rev] == revtodo:
468 468 pos += 1
469 469 ui.status(_('rebasing %s\n') % desc)
470 470 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
471 471 _('changesets'), total)
472 472 p1, p2, base = defineparents(repo, rev, self.destmap,
473 473 self.state, self.skipped,
474 474 self.obsoletenotrebased)
475 475 self.storestatus(tr=tr)
476 476 storecollapsemsg(repo, self.collapsemsg)
477 477 if len(repo[None].parents()) == 2:
478 478 repo.ui.debug('resuming interrupted rebase\n')
479 479 else:
480 480 try:
481 481 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
482 482 'rebase')
483 483 stats = rebasenode(repo, rev, p1, base, self.state,
484 484 self.collapsef, dest)
485 485 if stats and stats[3] > 0:
486 486 raise error.InterventionRequired(
487 487 _('unresolved conflicts (see hg '
488 488 'resolve, then hg rebase --continue)'))
489 489 finally:
490 490 ui.setconfig('ui', 'forcemerge', '', 'rebase')
491 491 if not self.collapsef:
492 492 merging = p2 != nullrev
493 493 editform = cmdutil.mergeeditform(merging, 'rebase')
494 494 editor = cmdutil.getcommiteditor(editform=editform, **opts)
495 495 newnode = concludenode(repo, rev, p1, p2,
496 496 extrafn=_makeextrafn(self.extrafns),
497 497 editor=editor,
498 498 keepbranches=self.keepbranchesf,
499 499 date=self.date)
500 500 if newnode is None:
501 501 # If it ended up being a no-op commit, then the normal
502 502 # merge state clean-up path doesn't happen, so do it
503 503 # here. Fix issue5494
504 504 mergemod.mergestate.clean(repo)
505 505 else:
506 506 # Skip commit if we are collapsing
507 507 repo.setparents(repo[p1].node())
508 508 newnode = None
509 509 # Update the state
510 510 if newnode is not None:
511 511 self.state[rev] = repo[newnode].rev()
512 512 ui.debug('rebased as %s\n' % short(newnode))
513 513 else:
514 514 if not self.collapsef:
515 515 ui.warn(_('note: rebase of %d:%s created no changes '
516 516 'to commit\n') % (rev, ctx))
517 517 self.skipped.add(rev)
518 518 self.state[rev] = p1
519 519 ui.debug('next revision set to %s\n' % p1)
520 520 else:
521 521 ui.status(_('already rebased %s as %s\n') %
522 522 (desc, repo[self.state[rev]]))
523 523 return pos
524 524
525 525 def _finishrebase(self):
526 526 repo, ui, opts = self.repo, self.ui, self.opts
527 527 fm = ui.formatter('rebase', opts)
528 528 fm.startitem()
529 529 if self.collapsef and not self.keepopen:
530 530 p1, p2, _base = defineparents(repo, min(self.state), self.destmap,
531 531 self.state, self.skipped,
532 532 self.obsoletenotrebased)
533 533 editopt = opts.get('edit')
534 534 editform = 'rebase.collapse'
535 535 if self.collapsemsg:
536 536 commitmsg = self.collapsemsg
537 537 else:
538 538 commitmsg = 'Collapsed revision'
539 539 for rebased in sorted(self.state):
540 540 if rebased not in self.skipped:
541 541 commitmsg += '\n* %s' % repo[rebased].description()
542 542 editopt = True
543 543 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
544 544 revtoreuse = max(self.state)
545 545
546 546 dsguard = None
547 547 if ui.configbool('rebase', 'singletransaction'):
548 548 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
549 549 with util.acceptintervention(dsguard):
550 550 newnode = concludenode(repo, revtoreuse, p1, self.external,
551 551 commitmsg=commitmsg,
552 552 extrafn=_makeextrafn(self.extrafns),
553 553 editor=editor,
554 554 keepbranches=self.keepbranchesf,
555 555 date=self.date)
556 556 if newnode is not None:
557 557 newrev = repo[newnode].rev()
558 558 for oldrev in self.state.iterkeys():
559 559 self.state[oldrev] = newrev
560 560
561 561 if 'qtip' in repo.tags():
562 562 updatemq(repo, self.state, self.skipped, **opts)
563 563
564 564 # restore original working directory
565 565 # (we do this before stripping)
566 566 newwd = self.state.get(self.originalwd, self.originalwd)
567 567 if newwd < 0:
568 568 # original directory is a parent of rebase set root or ignored
569 569 newwd = self.originalwd
570 570 if newwd not in [c.rev() for c in repo[None].parents()]:
571 571 ui.note(_("update back to initial working directory parent\n"))
572 572 hg.updaterepo(repo, newwd, False)
573 573
574 574 collapsedas = None
575 575 if not self.keepf:
576 576 if self.collapsef:
577 577 collapsedas = newnode
578 578 clearrebased(ui, repo, self.destmap, self.state, self.skipped,
579 579 collapsedas, self.keepf, fm=fm)
580 580
581 581 clearstatus(repo)
582 582 clearcollapsemsg(repo)
583 583
584 584 ui.note(_("rebase completed\n"))
585 585 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
586 586 if self.skipped:
587 587 skippedlen = len(self.skipped)
588 588 ui.note(_("%d revisions have been skipped\n") % skippedlen)
589 589 fm.end()
590 590
591 591 if (self.activebookmark and self.activebookmark in repo._bookmarks and
592 592 repo['.'].node() == repo._bookmarks[self.activebookmark]):
593 593 bookmarks.activate(repo, self.activebookmark)
594 594
595 595 @command('rebase',
596 596 [('s', 'source', '',
597 597 _('rebase the specified changeset and descendants'), _('REV')),
598 598 ('b', 'base', '',
599 599 _('rebase everything from branching point of specified changeset'),
600 600 _('REV')),
601 601 ('r', 'rev', [],
602 602 _('rebase these revisions'),
603 603 _('REV')),
604 604 ('d', 'dest', '',
605 605 _('rebase onto the specified changeset'), _('REV')),
606 606 ('', 'collapse', False, _('collapse the rebased changesets')),
607 607 ('m', 'message', '',
608 608 _('use text as collapse commit message'), _('TEXT')),
609 609 ('e', 'edit', False, _('invoke editor on commit messages')),
610 610 ('l', 'logfile', '',
611 611 _('read collapse commit message from file'), _('FILE')),
612 612 ('k', 'keep', False, _('keep original changesets')),
613 613 ('', 'keepbranches', False, _('keep original branch names')),
614 614 ('D', 'detach', False, _('(DEPRECATED)')),
615 615 ('i', 'interactive', False, _('(DEPRECATED)')),
616 616 ('t', 'tool', '', _('specify merge tool')),
617 617 ('c', 'continue', False, _('continue an interrupted rebase')),
618 618 ('', 'inmemory', False, _('run rebase in-memory (EXPERIMENTAL)')),
619 619 ('a', 'abort', False, _('abort an interrupted rebase'))] +
620 620 cmdutil.formatteropts,
621 621 _('[-s REV | -b REV] [-d REV] [OPTION]'))
622 622 def rebase(ui, repo, **opts):
623 623 """move changeset (and descendants) to a different branch
624 624
625 625 Rebase uses repeated merging to graft changesets from one part of
626 626 history (the source) onto another (the destination). This can be
627 627 useful for linearizing *local* changes relative to a master
628 628 development tree.
629 629
630 630 Published commits cannot be rebased (see :hg:`help phases`).
631 631 To copy commits, see :hg:`help graft`.
632 632
633 633 If you don't specify a destination changeset (``-d/--dest``), rebase
634 634 will use the same logic as :hg:`merge` to pick a destination. if
635 635 the current branch contains exactly one other head, the other head
636 636 is merged with by default. Otherwise, an explicit revision with
637 637 which to merge with must be provided. (destination changeset is not
638 638 modified by rebasing, but new changesets are added as its
639 639 descendants.)
640 640
641 641 Here are the ways to select changesets:
642 642
643 643 1. Explicitly select them using ``--rev``.
644 644
645 645 2. Use ``--source`` to select a root changeset and include all of its
646 646 descendants.
647 647
648 648 3. Use ``--base`` to select a changeset; rebase will find ancestors
649 649 and their descendants which are not also ancestors of the destination.
650 650
651 651 4. If you do not specify any of ``--rev``, ``source``, or ``--base``,
652 652 rebase will use ``--base .`` as above.
653 653
654 654 If ``--source`` or ``--rev`` is used, special names ``SRC`` and ``ALLSRC``
655 655 can be used in ``--dest``. Destination would be calculated per source
656 656 revision with ``SRC`` substituted by that single source revision and
657 657 ``ALLSRC`` substituted by all source revisions.
658 658
659 659 Rebase will destroy original changesets unless you use ``--keep``.
660 660 It will also move your bookmarks (even if you do).
661 661
662 662 Some changesets may be dropped if they do not contribute changes
663 663 (e.g. merges from the destination branch).
664 664
665 665 Unlike ``merge``, rebase will do nothing if you are at the branch tip of
666 666 a named branch with two heads. You will need to explicitly specify source
667 667 and/or destination.
668 668
669 669 If you need to use a tool to automate merge/conflict decisions, you
670 670 can specify one with ``--tool``, see :hg:`help merge-tools`.
671 671 As a caveat: the tool will not be used to mediate when a file was
672 672 deleted, there is no hook presently available for this.
673 673
674 674 If a rebase is interrupted to manually resolve a conflict, it can be
675 675 continued with --continue/-c or aborted with --abort/-a.
676 676
677 677 .. container:: verbose
678 678
679 679 Examples:
680 680
681 681 - move "local changes" (current commit back to branching point)
682 682 to the current branch tip after a pull::
683 683
684 684 hg rebase
685 685
686 686 - move a single changeset to the stable branch::
687 687
688 688 hg rebase -r 5f493448 -d stable
689 689
690 690 - splice a commit and all its descendants onto another part of history::
691 691
692 692 hg rebase --source c0c3 --dest 4cf9
693 693
694 694 - rebase everything on a branch marked by a bookmark onto the
695 695 default branch::
696 696
697 697 hg rebase --base myfeature --dest default
698 698
699 699 - collapse a sequence of changes into a single commit::
700 700
701 701 hg rebase --collapse -r 1520:1525 -d .
702 702
703 703 - move a named branch while preserving its name::
704 704
705 705 hg rebase -r "branch(featureX)" -d 1.3 --keepbranches
706 706
707 707 - stabilize orphaned changesets so history looks linear::
708 708
709 709 hg rebase -r 'orphan()-obsolete()'\
710 710 -d 'first(max((successors(max(roots(ALLSRC) & ::SRC)^)-obsolete())::) +\
711 711 max(::((roots(ALLSRC) & ::SRC)^)-obsolete()))'
712 712
713 713 Configuration Options:
714 714
715 715 You can make rebase require a destination if you set the following config
716 716 option::
717 717
718 718 [commands]
719 719 rebase.requiredest = True
720 720
721 721 By default, rebase will close the transaction after each commit. For
722 722 performance purposes, you can configure rebase to use a single transaction
723 723 across the entire rebase. WARNING: This setting introduces a significant
724 724 risk of losing the work you've done in a rebase if the rebase aborts
725 725 unexpectedly::
726 726
727 727 [rebase]
728 728 singletransaction = True
729 729
730 730 Return Values:
731 731
732 732 Returns 0 on success, 1 if nothing to rebase or there are
733 733 unresolved conflicts.
734 734
735 735 """
736 736 opts = pycompat.byteskwargs(opts)
737 737 if 'inmemory' not in opts:
738 738 opts['inmemory'] = False
739 739 rbsrt = rebaseruntime(repo, ui, opts)
740 740
741 741 with repo.wlock(), repo.lock():
742 742 # Validate input and define rebasing points
743 743 destf = opts.get('dest', None)
744 744 srcf = opts.get('source', None)
745 745 basef = opts.get('base', None)
746 746 revf = opts.get('rev', [])
747 747 # search default destination in this space
748 748 # used in the 'hg pull --rebase' case, see issue 5214.
749 749 destspace = opts.get('_destspace')
750 750 contf = opts.get('continue')
751 751 abortf = opts.get('abort')
752 752 if opts.get('interactive'):
753 753 try:
754 754 if extensions.find('histedit'):
755 755 enablehistedit = ''
756 756 except KeyError:
757 757 enablehistedit = " --config extensions.histedit="
758 758 help = "hg%s help -e histedit" % enablehistedit
759 759 msg = _("interactive history editing is supported by the "
760 760 "'histedit' extension (see \"%s\")") % help
761 761 raise error.Abort(msg)
762 762
763 763 if rbsrt.collapsemsg and not rbsrt.collapsef:
764 764 raise error.Abort(
765 765 _('message can only be specified with collapse'))
766 766
767 767 if contf or abortf:
768 768 if contf and abortf:
769 769 raise error.Abort(_('cannot use both abort and continue'))
770 770 if rbsrt.collapsef:
771 771 raise error.Abort(
772 772 _('cannot use collapse with continue or abort'))
773 773 if srcf or basef or destf:
774 774 raise error.Abort(
775 775 _('abort and continue do not allow specifying revisions'))
776 776 if abortf and opts.get('tool', False):
777 777 ui.warn(_('tool option will be ignored\n'))
778 778 if contf:
779 779 ms = mergemod.mergestate.read(repo)
780 780 mergeutil.checkunresolved(ms)
781 781
782 782 retcode = rbsrt._prepareabortorcontinue(abortf)
783 783 if retcode is not None:
784 784 return retcode
785 785 else:
786 786 destmap = _definedestmap(ui, repo, destf, srcf, basef, revf,
787 destspace=destspace)
787 destspace=destspace,
788 inmemory=opts['inmemory'])
788 789 retcode = rbsrt._preparenewrebase(destmap)
789 790 if retcode is not None:
790 791 return retcode
791 792
792 793 tr = None
793 794 dsguard = None
794 795
795 796 singletr = ui.configbool('rebase', 'singletransaction')
796 797 if singletr:
797 798 tr = repo.transaction('rebase')
798 799 with util.acceptintervention(tr):
799 800 if singletr:
800 801 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
801 802 with util.acceptintervention(dsguard):
802 803 rbsrt._performrebase(tr)
803 804
804 805 rbsrt._finishrebase()
805 806
806 807 def _definedestmap(ui, repo, destf=None, srcf=None, basef=None, revf=None,
807 destspace=None):
808 destspace=None, inmemory=False):
808 809 """use revisions argument to define destmap {srcrev: destrev}"""
809 810 if revf is None:
810 811 revf = []
811 812
812 813 # destspace is here to work around issues with `hg pull --rebase` see
813 814 # issue5214 for details
814 815 if srcf and basef:
815 816 raise error.Abort(_('cannot specify both a source and a base'))
816 817 if revf and basef:
817 818 raise error.Abort(_('cannot specify both a revision and a base'))
818 819 if revf and srcf:
819 820 raise error.Abort(_('cannot specify both a revision and a source'))
820 821
822 if not inmemory:
821 823 cmdutil.checkunfinished(repo)
822 824 cmdutil.bailifchanged(repo)
823 825
824 826 if ui.configbool('commands', 'rebase.requiredest') and not destf:
825 827 raise error.Abort(_('you must specify a destination'),
826 828 hint=_('use: hg rebase -d REV'))
827 829
828 830 dest = None
829 831
830 832 if revf:
831 833 rebaseset = scmutil.revrange(repo, revf)
832 834 if not rebaseset:
833 835 ui.status(_('empty "rev" revision set - nothing to rebase\n'))
834 836 return None
835 837 elif srcf:
836 838 src = scmutil.revrange(repo, [srcf])
837 839 if not src:
838 840 ui.status(_('empty "source" revision set - nothing to rebase\n'))
839 841 return None
840 842 rebaseset = repo.revs('(%ld)::', src)
841 843 assert rebaseset
842 844 else:
843 845 base = scmutil.revrange(repo, [basef or '.'])
844 846 if not base:
845 847 ui.status(_('empty "base" revision set - '
846 848 "can't compute rebase set\n"))
847 849 return None
848 850 if destf:
849 851 # --base does not support multiple destinations
850 852 dest = scmutil.revsingle(repo, destf)
851 853 else:
852 854 dest = repo[_destrebase(repo, base, destspace=destspace)]
853 855 destf = str(dest)
854 856
855 857 roots = [] # selected children of branching points
856 858 bpbase = {} # {branchingpoint: [origbase]}
857 859 for b in base: # group bases by branching points
858 860 bp = repo.revs('ancestor(%d, %d)', b, dest).first()
859 861 bpbase[bp] = bpbase.get(bp, []) + [b]
860 862 if None in bpbase:
861 863 # emulate the old behavior, showing "nothing to rebase" (a better
862 864 # behavior may be abort with "cannot find branching point" error)
863 865 bpbase.clear()
864 866 for bp, bs in bpbase.iteritems(): # calculate roots
865 867 roots += list(repo.revs('children(%d) & ancestors(%ld)', bp, bs))
866 868
867 869 rebaseset = repo.revs('%ld::', roots)
868 870
869 871 if not rebaseset:
870 872 # transform to list because smartsets are not comparable to
871 873 # lists. This should be improved to honor laziness of
872 874 # smartset.
873 875 if list(base) == [dest.rev()]:
874 876 if basef:
875 877 ui.status(_('nothing to rebase - %s is both "base"'
876 878 ' and destination\n') % dest)
877 879 else:
878 880 ui.status(_('nothing to rebase - working directory '
879 881 'parent is also destination\n'))
880 882 elif not repo.revs('%ld - ::%d', base, dest):
881 883 if basef:
882 884 ui.status(_('nothing to rebase - "base" %s is '
883 885 'already an ancestor of destination '
884 886 '%s\n') %
885 887 ('+'.join(str(repo[r]) for r in base),
886 888 dest))
887 889 else:
888 890 ui.status(_('nothing to rebase - working '
889 891 'directory parent is already an '
890 892 'ancestor of destination %s\n') % dest)
891 893 else: # can it happen?
892 894 ui.status(_('nothing to rebase from %s to %s\n') %
893 895 ('+'.join(str(repo[r]) for r in base), dest))
894 896 return None
895 897
896 898 if not destf:
897 899 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
898 900 destf = str(dest)
899 901
900 902 allsrc = revsetlang.formatspec('%ld', rebaseset)
901 903 alias = {'ALLSRC': allsrc}
902 904
903 905 if dest is None:
904 906 try:
905 907 # fast path: try to resolve dest without SRC alias
906 908 dest = scmutil.revsingle(repo, destf, localalias=alias)
907 909 except error.RepoLookupError:
908 910 # multi-dest path: resolve dest for each SRC separately
909 911 destmap = {}
910 912 for r in rebaseset:
911 913 alias['SRC'] = revsetlang.formatspec('%d', r)
912 914 # use repo.anyrevs instead of scmutil.revsingle because we
913 915 # don't want to abort if destset is empty.
914 916 destset = repo.anyrevs([destf], user=True, localalias=alias)
915 917 size = len(destset)
916 918 if size == 1:
917 919 destmap[r] = destset.first()
918 920 elif size == 0:
919 921 ui.note(_('skipping %s - empty destination\n') % repo[r])
920 922 else:
921 923 raise error.Abort(_('rebase destination for %s is not '
922 924 'unique') % repo[r])
923 925
924 926 if dest is not None:
925 927 # single-dest case: assign dest to each rev in rebaseset
926 928 destrev = dest.rev()
927 929 destmap = {r: destrev for r in rebaseset} # {srcrev: destrev}
928 930
929 931 if not destmap:
930 932 ui.status(_('nothing to rebase - empty destination\n'))
931 933 return None
932 934
933 935 return destmap
934 936
935 937 def externalparent(repo, state, destancestors):
936 938 """Return the revision that should be used as the second parent
937 939 when the revisions in state is collapsed on top of destancestors.
938 940 Abort if there is more than one parent.
939 941 """
940 942 parents = set()
941 943 source = min(state)
942 944 for rev in state:
943 945 if rev == source:
944 946 continue
945 947 for p in repo[rev].parents():
946 948 if (p.rev() not in state
947 949 and p.rev() not in destancestors):
948 950 parents.add(p.rev())
949 951 if not parents:
950 952 return nullrev
951 953 if len(parents) == 1:
952 954 return parents.pop()
953 955 raise error.Abort(_('unable to collapse on top of %s, there is more '
954 956 'than one external parent: %s') %
955 957 (max(destancestors),
956 958 ', '.join(str(p) for p in sorted(parents))))
957 959
958 960 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None,
959 961 keepbranches=False, date=None):
960 962 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
961 963 but also store useful information in extra.
962 964 Return node of committed revision.'''
963 965 dsguard = util.nullcontextmanager()
964 966 if not repo.ui.configbool('rebase', 'singletransaction'):
965 967 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
966 968 with dsguard:
967 969 repo.setparents(repo[p1].node(), repo[p2].node())
968 970 ctx = repo[rev]
969 971 if commitmsg is None:
970 972 commitmsg = ctx.description()
971 973 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
972 974 extra = {'rebase_source': ctx.hex()}
973 975 if extrafn:
974 976 extrafn(ctx, extra)
975 977
976 978 destphase = max(ctx.phase(), phases.draft)
977 979 overrides = {('phases', 'new-commit'): destphase}
978 980 with repo.ui.configoverride(overrides, 'rebase'):
979 981 if keepbranch:
980 982 repo.ui.setconfig('ui', 'allowemptycommit', True)
981 983 # Commit might fail if unresolved files exist
982 984 if date is None:
983 985 date = ctx.date()
984 986 newnode = repo.commit(text=commitmsg, user=ctx.user(),
985 987 date=date, extra=extra, editor=editor)
986 988
987 989 repo.dirstate.setbranch(repo[newnode].branch())
988 990 return newnode
989 991
990 992 def rebasenode(repo, rev, p1, base, state, collapse, dest):
991 993 'Rebase a single revision rev on top of p1 using base as merge ancestor'
992 994 # Merge phase
993 995 # Update to destination and merge it with local
994 996 if repo['.'].rev() != p1:
995 997 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
996 998 mergemod.update(repo, p1, False, True)
997 999 else:
998 1000 repo.ui.debug(" already in destination\n")
999 1001 repo.dirstate.write(repo.currenttransaction())
1000 1002 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
1001 1003 if base is not None:
1002 1004 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
1003 1005 # When collapsing in-place, the parent is the common ancestor, we
1004 1006 # have to allow merging with it.
1005 1007 wctx = repo[None]
1006 1008 stats = mergemod.update(repo, rev, True, True, base, collapse,
1007 1009 labels=['dest', 'source'])
1008 1010 if collapse:
1009 1011 copies.duplicatecopies(repo, wctx, rev, dest)
1010 1012 else:
1011 1013 # If we're not using --collapse, we need to
1012 1014 # duplicate copies between the revision we're
1013 1015 # rebasing and its first parent, but *not*
1014 1016 # duplicate any copies that have already been
1015 1017 # performed in the destination.
1016 1018 p1rev = repo[rev].p1().rev()
1017 1019 copies.duplicatecopies(repo, wctx, rev, p1rev, skiprev=dest)
1018 1020 return stats
1019 1021
1020 1022 def adjustdest(repo, rev, destmap, state, skipped):
1021 1023 """adjust rebase destination given the current rebase state
1022 1024
1023 1025 rev is what is being rebased. Return a list of two revs, which are the
1024 1026 adjusted destinations for rev's p1 and p2, respectively. If a parent is
1025 1027 nullrev, return dest without adjustment for it.
1026 1028
1027 1029 For example, when doing rebasing B+E to F, C to G, rebase will first move B
1028 1030 to B1, and E's destination will be adjusted from F to B1.
1029 1031
1030 1032 B1 <- written during rebasing B
1031 1033 |
1032 1034 F <- original destination of B, E
1033 1035 |
1034 1036 | E <- rev, which is being rebased
1035 1037 | |
1036 1038 | D <- prev, one parent of rev being checked
1037 1039 | |
1038 1040 | x <- skipped, ex. no successor or successor in (::dest)
1039 1041 | |
1040 1042 | C <- rebased as C', different destination
1041 1043 | |
1042 1044 | B <- rebased as B1 C'
1043 1045 |/ |
1044 1046 A G <- destination of C, different
1045 1047
1046 1048 Another example about merge changeset, rebase -r C+G+H -d K, rebase will
1047 1049 first move C to C1, G to G1, and when it's checking H, the adjusted
1048 1050 destinations will be [C1, G1].
1049 1051
1050 1052 H C1 G1
1051 1053 /| | /
1052 1054 F G |/
1053 1055 K | | -> K
1054 1056 | C D |
1055 1057 | |/ |
1056 1058 | B | ...
1057 1059 |/ |/
1058 1060 A A
1059 1061
1060 1062 Besides, adjust dest according to existing rebase information. For example,
1061 1063
1062 1064 B C D B needs to be rebased on top of C, C needs to be rebased on top
1063 1065 \|/ of D. We will rebase C first.
1064 1066 A
1065 1067
1066 1068 C' After rebasing C, when considering B's destination, use C'
1067 1069 | instead of the original C.
1068 1070 B D
1069 1071 \ /
1070 1072 A
1071 1073 """
1072 1074 # pick already rebased revs with same dest from state as interesting source
1073 1075 dest = destmap[rev]
1074 1076 source = [s for s, d in state.items()
1075 1077 if d > 0 and destmap[s] == dest and s not in skipped]
1076 1078
1077 1079 result = []
1078 1080 for prev in repo.changelog.parentrevs(rev):
1079 1081 adjusted = dest
1080 1082 if prev != nullrev:
1081 1083 candidate = repo.revs('max(%ld and (::%d))', source, prev).first()
1082 1084 if candidate is not None:
1083 1085 adjusted = state[candidate]
1084 1086 if adjusted == dest and dest in state:
1085 1087 adjusted = state[dest]
1086 1088 if adjusted == revtodo:
1087 1089 # sortsource should produce an order that makes this impossible
1088 1090 raise error.ProgrammingError(
1089 1091 'rev %d should be rebased already at this time' % dest)
1090 1092 result.append(adjusted)
1091 1093 return result
1092 1094
1093 1095 def _checkobsrebase(repo, ui, rebaseobsrevs, rebaseobsskipped):
1094 1096 """
1095 1097 Abort if rebase will create divergence or rebase is noop because of markers
1096 1098
1097 1099 `rebaseobsrevs`: set of obsolete revision in source
1098 1100 `rebaseobsskipped`: set of revisions from source skipped because they have
1099 1101 successors in destination
1100 1102 """
1101 1103 # Obsolete node with successors not in dest leads to divergence
1102 1104 divergenceok = ui.configbool('experimental',
1103 1105 'evolution.allowdivergence')
1104 1106 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
1105 1107
1106 1108 if divergencebasecandidates and not divergenceok:
1107 1109 divhashes = (str(repo[r])
1108 1110 for r in divergencebasecandidates)
1109 1111 msg = _("this rebase will cause "
1110 1112 "divergences from: %s")
1111 1113 h = _("to force the rebase please set "
1112 1114 "experimental.evolution.allowdivergence=True")
1113 1115 raise error.Abort(msg % (",".join(divhashes),), hint=h)
1114 1116
1115 1117 def successorrevs(unfi, rev):
1116 1118 """yield revision numbers for successors of rev"""
1117 1119 assert unfi.filtername is None
1118 1120 nodemap = unfi.changelog.nodemap
1119 1121 for s in obsutil.allsuccessors(unfi.obsstore, [unfi[rev].node()]):
1120 1122 if s in nodemap:
1121 1123 yield nodemap[s]
1122 1124
1123 1125 def defineparents(repo, rev, destmap, state, skipped, obsskipped):
1124 1126 """Return new parents and optionally a merge base for rev being rebased
1125 1127
1126 1128 The destination specified by "dest" cannot always be used directly because
1127 1129 previously rebase result could affect destination. For example,
1128 1130
1129 1131 D E rebase -r C+D+E -d B
1130 1132 |/ C will be rebased to C'
1131 1133 B C D's new destination will be C' instead of B
1132 1134 |/ E's new destination will be C' instead of B
1133 1135 A
1134 1136
1135 1137 The new parents of a merge is slightly more complicated. See the comment
1136 1138 block below.
1137 1139 """
1138 1140 # use unfiltered changelog since successorrevs may return filtered nodes
1139 1141 assert repo.filtername is None
1140 1142 cl = repo.changelog
1141 1143 def isancestor(a, b):
1142 1144 # take revision numbers instead of nodes
1143 1145 if a == b:
1144 1146 return True
1145 1147 elif a > b:
1146 1148 return False
1147 1149 return cl.isancestor(cl.node(a), cl.node(b))
1148 1150
1149 1151 dest = destmap[rev]
1150 1152 oldps = repo.changelog.parentrevs(rev) # old parents
1151 1153 newps = [nullrev, nullrev] # new parents
1152 1154 dests = adjustdest(repo, rev, destmap, state, skipped)
1153 1155 bases = list(oldps) # merge base candidates, initially just old parents
1154 1156
1155 1157 if all(r == nullrev for r in oldps[1:]):
1156 1158 # For non-merge changeset, just move p to adjusted dest as requested.
1157 1159 newps[0] = dests[0]
1158 1160 else:
1159 1161 # For merge changeset, if we move p to dests[i] unconditionally, both
1160 1162 # parents may change and the end result looks like "the merge loses a
1161 1163 # parent", which is a surprise. This is a limit because "--dest" only
1162 1164 # accepts one dest per src.
1163 1165 #
1164 1166 # Therefore, only move p with reasonable conditions (in this order):
1165 1167 # 1. use dest, if dest is a descendent of (p or one of p's successors)
1166 1168 # 2. use p's rebased result, if p is rebased (state[p] > 0)
1167 1169 #
1168 1170 # Comparing with adjustdest, the logic here does some additional work:
1169 1171 # 1. decide which parents will not be moved towards dest
1170 1172 # 2. if the above decision is "no", should a parent still be moved
1171 1173 # because it was rebased?
1172 1174 #
1173 1175 # For example:
1174 1176 #
1175 1177 # C # "rebase -r C -d D" is an error since none of the parents
1176 1178 # /| # can be moved. "rebase -r B+C -d D" will move C's parent
1177 1179 # A B D # B (using rule "2."), since B will be rebased.
1178 1180 #
1179 1181 # The loop tries to be not rely on the fact that a Mercurial node has
1180 1182 # at most 2 parents.
1181 1183 for i, p in enumerate(oldps):
1182 1184 np = p # new parent
1183 1185 if any(isancestor(x, dests[i]) for x in successorrevs(repo, p)):
1184 1186 np = dests[i]
1185 1187 elif p in state and state[p] > 0:
1186 1188 np = state[p]
1187 1189
1188 1190 # "bases" only record "special" merge bases that cannot be
1189 1191 # calculated from changelog DAG (i.e. isancestor(p, np) is False).
1190 1192 # For example:
1191 1193 #
1192 1194 # B' # rebase -s B -d D, when B was rebased to B'. dest for C
1193 1195 # | C # is B', but merge base for C is B, instead of
1194 1196 # D | # changelog.ancestor(C, B') == A. If changelog DAG and
1195 1197 # | B # "state" edges are merged (so there will be an edge from
1196 1198 # |/ # B to B'), the merge base is still ancestor(C, B') in
1197 1199 # A # the merged graph.
1198 1200 #
1199 1201 # Also see https://bz.mercurial-scm.org/show_bug.cgi?id=1950#c8
1200 1202 # which uses "virtual null merge" to explain this situation.
1201 1203 if isancestor(p, np):
1202 1204 bases[i] = nullrev
1203 1205
1204 1206 # If one parent becomes an ancestor of the other, drop the ancestor
1205 1207 for j, x in enumerate(newps[:i]):
1206 1208 if x == nullrev:
1207 1209 continue
1208 1210 if isancestor(np, x): # CASE-1
1209 1211 np = nullrev
1210 1212 elif isancestor(x, np): # CASE-2
1211 1213 newps[j] = np
1212 1214 np = nullrev
1213 1215 # New parents forming an ancestor relationship does not
1214 1216 # mean the old parents have a similar relationship. Do not
1215 1217 # set bases[x] to nullrev.
1216 1218 bases[j], bases[i] = bases[i], bases[j]
1217 1219
1218 1220 newps[i] = np
1219 1221
1220 1222 # "rebasenode" updates to new p1, and the old p1 will be used as merge
1221 1223 # base. If only p2 changes, merging using unchanged p1 as merge base is
1222 1224 # suboptimal. Therefore swap parents to make the merge sane.
1223 1225 if newps[1] != nullrev and oldps[0] == newps[0]:
1224 1226 assert len(newps) == 2 and len(oldps) == 2
1225 1227 newps.reverse()
1226 1228 bases.reverse()
1227 1229
1228 1230 # No parent change might be an error because we fail to make rev a
1229 1231 # descendent of requested dest. This can happen, for example:
1230 1232 #
1231 1233 # C # rebase -r C -d D
1232 1234 # /| # None of A and B will be changed to D and rebase fails.
1233 1235 # A B D
1234 1236 if set(newps) == set(oldps) and dest not in newps:
1235 1237 raise error.Abort(_('cannot rebase %d:%s without '
1236 1238 'moving at least one of its parents')
1237 1239 % (rev, repo[rev]))
1238 1240
1239 1241 # Source should not be ancestor of dest. The check here guarantees it's
1240 1242 # impossible. With multi-dest, the initial check does not cover complex
1241 1243 # cases since we don't have abstractions to dry-run rebase cheaply.
1242 1244 if any(p != nullrev and isancestor(rev, p) for p in newps):
1243 1245 raise error.Abort(_('source is ancestor of destination'))
1244 1246
1245 1247 # "rebasenode" updates to new p1, use the corresponding merge base.
1246 1248 if bases[0] != nullrev:
1247 1249 base = bases[0]
1248 1250 else:
1249 1251 base = None
1250 1252
1251 1253 # Check if the merge will contain unwanted changes. That may happen if
1252 1254 # there are multiple special (non-changelog ancestor) merge bases, which
1253 1255 # cannot be handled well by the 3-way merge algorithm. For example:
1254 1256 #
1255 1257 # F
1256 1258 # /|
1257 1259 # D E # "rebase -r D+E+F -d Z", when rebasing F, if "D" was chosen
1258 1260 # | | # as merge base, the difference between D and F will include
1259 1261 # B C # C, so the rebased F will contain C surprisingly. If "E" was
1260 1262 # |/ # chosen, the rebased F will contain B.
1261 1263 # A Z
1262 1264 #
1263 1265 # But our merge base candidates (D and E in above case) could still be
1264 1266 # better than the default (ancestor(F, Z) == null). Therefore still
1265 1267 # pick one (so choose p1 above).
1266 1268 if sum(1 for b in bases if b != nullrev) > 1:
1267 1269 unwanted = [None, None] # unwanted[i]: unwanted revs if choose bases[i]
1268 1270 for i, base in enumerate(bases):
1269 1271 if base == nullrev:
1270 1272 continue
1271 1273 # Revisions in the side (not chosen as merge base) branch that
1272 1274 # might contain "surprising" contents
1273 1275 siderevs = list(repo.revs('((%ld-%d) %% (%d+%d))',
1274 1276 bases, base, base, dest))
1275 1277
1276 1278 # If those revisions are covered by rebaseset, the result is good.
1277 1279 # A merge in rebaseset would be considered to cover its ancestors.
1278 1280 if siderevs:
1279 1281 rebaseset = [r for r, d in state.items()
1280 1282 if d > 0 and r not in obsskipped]
1281 1283 merges = [r for r in rebaseset
1282 1284 if cl.parentrevs(r)[1] != nullrev]
1283 1285 unwanted[i] = list(repo.revs('%ld - (::%ld) - %ld',
1284 1286 siderevs, merges, rebaseset))
1285 1287
1286 1288 # Choose a merge base that has a minimal number of unwanted revs.
1287 1289 l, i = min((len(revs), i)
1288 1290 for i, revs in enumerate(unwanted) if revs is not None)
1289 1291 base = bases[i]
1290 1292
1291 1293 # newps[0] should match merge base if possible. Currently, if newps[i]
1292 1294 # is nullrev, the only case is newps[i] and newps[j] (j < i), one is
1293 1295 # the other's ancestor. In that case, it's fine to not swap newps here.
1294 1296 # (see CASE-1 and CASE-2 above)
1295 1297 if i != 0 and newps[i] != nullrev:
1296 1298 newps[0], newps[i] = newps[i], newps[0]
1297 1299
1298 1300 # The merge will include unwanted revisions. Abort now. Revisit this if
1299 1301 # we have a more advanced merge algorithm that handles multiple bases.
1300 1302 if l > 0:
1301 1303 unwanteddesc = _(' or ').join(
1302 1304 (', '.join('%d:%s' % (r, repo[r]) for r in revs)
1303 1305 for revs in unwanted if revs is not None))
1304 1306 raise error.Abort(
1305 1307 _('rebasing %d:%s will include unwanted changes from %s')
1306 1308 % (rev, repo[rev], unwanteddesc))
1307 1309
1308 1310 repo.ui.debug(" future parents are %d and %d\n" % tuple(newps))
1309 1311
1310 1312 return newps[0], newps[1], base
1311 1313
1312 1314 def isagitpatch(repo, patchname):
1313 1315 'Return true if the given patch is in git format'
1314 1316 mqpatch = os.path.join(repo.mq.path, patchname)
1315 1317 for line in patch.linereader(file(mqpatch, 'rb')):
1316 1318 if line.startswith('diff --git'):
1317 1319 return True
1318 1320 return False
1319 1321
1320 1322 def updatemq(repo, state, skipped, **opts):
1321 1323 'Update rebased mq patches - finalize and then import them'
1322 1324 mqrebase = {}
1323 1325 mq = repo.mq
1324 1326 original_series = mq.fullseries[:]
1325 1327 skippedpatches = set()
1326 1328
1327 1329 for p in mq.applied:
1328 1330 rev = repo[p.node].rev()
1329 1331 if rev in state:
1330 1332 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
1331 1333 (rev, p.name))
1332 1334 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
1333 1335 else:
1334 1336 # Applied but not rebased, not sure this should happen
1335 1337 skippedpatches.add(p.name)
1336 1338
1337 1339 if mqrebase:
1338 1340 mq.finish(repo, mqrebase.keys())
1339 1341
1340 1342 # We must start import from the newest revision
1341 1343 for rev in sorted(mqrebase, reverse=True):
1342 1344 if rev not in skipped:
1343 1345 name, isgit = mqrebase[rev]
1344 1346 repo.ui.note(_('updating mq patch %s to %s:%s\n') %
1345 1347 (name, state[rev], repo[state[rev]]))
1346 1348 mq.qimport(repo, (), patchname=name, git=isgit,
1347 1349 rev=[str(state[rev])])
1348 1350 else:
1349 1351 # Rebased and skipped
1350 1352 skippedpatches.add(mqrebase[rev][0])
1351 1353
1352 1354 # Patches were either applied and rebased and imported in
1353 1355 # order, applied and removed or unapplied. Discard the removed
1354 1356 # ones while preserving the original series order and guards.
1355 1357 newseries = [s for s in original_series
1356 1358 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
1357 1359 mq.fullseries[:] = newseries
1358 1360 mq.seriesdirty = True
1359 1361 mq.savedirty()
1360 1362
1361 1363 def storecollapsemsg(repo, collapsemsg):
1362 1364 'Store the collapse message to allow recovery'
1363 1365 collapsemsg = collapsemsg or ''
1364 1366 f = repo.vfs("last-message.txt", "w")
1365 1367 f.write("%s\n" % collapsemsg)
1366 1368 f.close()
1367 1369
1368 1370 def clearcollapsemsg(repo):
1369 1371 'Remove collapse message file'
1370 1372 repo.vfs.unlinkpath("last-message.txt", ignoremissing=True)
1371 1373
1372 1374 def restorecollapsemsg(repo, isabort):
1373 1375 'Restore previously stored collapse message'
1374 1376 try:
1375 1377 f = repo.vfs("last-message.txt")
1376 1378 collapsemsg = f.readline().strip()
1377 1379 f.close()
1378 1380 except IOError as err:
1379 1381 if err.errno != errno.ENOENT:
1380 1382 raise
1381 1383 if isabort:
1382 1384 # Oh well, just abort like normal
1383 1385 collapsemsg = ''
1384 1386 else:
1385 1387 raise error.Abort(_('missing .hg/last-message.txt for rebase'))
1386 1388 return collapsemsg
1387 1389
1388 1390 def clearstatus(repo):
1389 1391 'Remove the status files'
1390 1392 # Make sure the active transaction won't write the state file
1391 1393 tr = repo.currenttransaction()
1392 1394 if tr:
1393 1395 tr.removefilegenerator('rebasestate')
1394 1396 repo.vfs.unlinkpath("rebasestate", ignoremissing=True)
1395 1397
1396 1398 def needupdate(repo, state):
1397 1399 '''check whether we should `update --clean` away from a merge, or if
1398 1400 somehow the working dir got forcibly updated, e.g. by older hg'''
1399 1401 parents = [p.rev() for p in repo[None].parents()]
1400 1402
1401 1403 # Are we in a merge state at all?
1402 1404 if len(parents) < 2:
1403 1405 return False
1404 1406
1405 1407 # We should be standing on the first as-of-yet unrebased commit.
1406 1408 firstunrebased = min([old for old, new in state.iteritems()
1407 1409 if new == nullrev])
1408 1410 if firstunrebased in parents:
1409 1411 return True
1410 1412
1411 1413 return False
1412 1414
1413 1415 def abort(repo, originalwd, destmap, state, activebookmark=None):
1414 1416 '''Restore the repository to its original state. Additional args:
1415 1417
1416 1418 activebookmark: the name of the bookmark that should be active after the
1417 1419 restore'''
1418 1420
1419 1421 try:
1420 1422 # If the first commits in the rebased set get skipped during the rebase,
1421 1423 # their values within the state mapping will be the dest rev id. The
1422 1424 # dstates list must must not contain the dest rev (issue4896)
1423 1425 dstates = [s for r, s in state.items() if s >= 0 and s != destmap[r]]
1424 1426 immutable = [d for d in dstates if not repo[d].mutable()]
1425 1427 cleanup = True
1426 1428 if immutable:
1427 1429 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
1428 1430 % ', '.join(str(repo[r]) for r in immutable),
1429 1431 hint=_("see 'hg help phases' for details"))
1430 1432 cleanup = False
1431 1433
1432 1434 descendants = set()
1433 1435 if dstates:
1434 1436 descendants = set(repo.changelog.descendants(dstates))
1435 1437 if descendants - set(dstates):
1436 1438 repo.ui.warn(_("warning: new changesets detected on destination "
1437 1439 "branch, can't strip\n"))
1438 1440 cleanup = False
1439 1441
1440 1442 if cleanup:
1441 1443 shouldupdate = False
1442 1444 rebased = [s for r, s in state.items()
1443 1445 if s >= 0 and s != destmap[r]]
1444 1446 if rebased:
1445 1447 strippoints = [
1446 1448 c.node() for c in repo.set('roots(%ld)', rebased)]
1447 1449
1448 1450 updateifonnodes = set(rebased)
1449 1451 updateifonnodes.update(destmap.values())
1450 1452 updateifonnodes.add(originalwd)
1451 1453 shouldupdate = repo['.'].rev() in updateifonnodes
1452 1454
1453 1455 # Update away from the rebase if necessary
1454 1456 if shouldupdate or needupdate(repo, state):
1455 1457 mergemod.update(repo, originalwd, False, True)
1456 1458
1457 1459 # Strip from the first rebased revision
1458 1460 if rebased:
1459 1461 # no backup of rebased cset versions needed
1460 1462 repair.strip(repo.ui, repo, strippoints)
1461 1463
1462 1464 if activebookmark and activebookmark in repo._bookmarks:
1463 1465 bookmarks.activate(repo, activebookmark)
1464 1466
1465 1467 finally:
1466 1468 clearstatus(repo)
1467 1469 clearcollapsemsg(repo)
1468 1470 repo.ui.warn(_('rebase aborted\n'))
1469 1471 return 0
1470 1472
1471 1473 def sortsource(destmap):
1472 1474 """yield source revisions in an order that we only rebase things once
1473 1475
1474 1476 If source and destination overlaps, we should filter out revisions
1475 1477 depending on other revisions which hasn't been rebased yet.
1476 1478
1477 1479 Yield a sorted list of revisions each time.
1478 1480
1479 1481 For example, when rebasing A to B, B to C. This function yields [B], then
1480 1482 [A], indicating B needs to be rebased first.
1481 1483
1482 1484 Raise if there is a cycle so the rebase is impossible.
1483 1485 """
1484 1486 srcset = set(destmap)
1485 1487 while srcset:
1486 1488 srclist = sorted(srcset)
1487 1489 result = []
1488 1490 for r in srclist:
1489 1491 if destmap[r] not in srcset:
1490 1492 result.append(r)
1491 1493 if not result:
1492 1494 raise error.Abort(_('source and destination form a cycle'))
1493 1495 srcset -= set(result)
1494 1496 yield result
1495 1497
1496 1498 def buildstate(repo, destmap, collapse):
1497 1499 '''Define which revisions are going to be rebased and where
1498 1500
1499 1501 repo: repo
1500 1502 destmap: {srcrev: destrev}
1501 1503 '''
1502 1504 rebaseset = destmap.keys()
1503 1505 originalwd = repo['.'].rev()
1504 1506
1505 1507 # This check isn't strictly necessary, since mq detects commits over an
1506 1508 # applied patch. But it prevents messing up the working directory when
1507 1509 # a partially completed rebase is blocked by mq.
1508 1510 if 'qtip' in repo.tags():
1509 1511 mqapplied = set(repo[s.node].rev() for s in repo.mq.applied)
1510 1512 if set(destmap.values()) & mqapplied:
1511 1513 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1512 1514
1513 1515 # Get "cycle" error early by exhausting the generator.
1514 1516 sortedsrc = list(sortsource(destmap)) # a list of sorted revs
1515 1517 if not sortedsrc:
1516 1518 raise error.Abort(_('no matching revisions'))
1517 1519
1518 1520 # Only check the first batch of revisions to rebase not depending on other
1519 1521 # rebaseset. This means "source is ancestor of destination" for the second
1520 1522 # (and following) batches of revisions are not checked here. We rely on
1521 1523 # "defineparents" to do that check.
1522 1524 roots = list(repo.set('roots(%ld)', sortedsrc[0]))
1523 1525 if not roots:
1524 1526 raise error.Abort(_('no matching revisions'))
1525 1527 roots.sort()
1526 1528 state = dict.fromkeys(rebaseset, revtodo)
1527 1529 emptyrebase = (len(sortedsrc) == 1)
1528 1530 for root in roots:
1529 1531 dest = repo[destmap[root.rev()]]
1530 1532 commonbase = root.ancestor(dest)
1531 1533 if commonbase == root:
1532 1534 raise error.Abort(_('source is ancestor of destination'))
1533 1535 if commonbase == dest:
1534 1536 wctx = repo[None]
1535 1537 if dest == wctx.p1():
1536 1538 # when rebasing to '.', it will use the current wd branch name
1537 1539 samebranch = root.branch() == wctx.branch()
1538 1540 else:
1539 1541 samebranch = root.branch() == dest.branch()
1540 1542 if not collapse and samebranch and dest in root.parents():
1541 1543 # mark the revision as done by setting its new revision
1542 1544 # equal to its old (current) revisions
1543 1545 state[root.rev()] = root.rev()
1544 1546 repo.ui.debug('source is a child of destination\n')
1545 1547 continue
1546 1548
1547 1549 emptyrebase = False
1548 1550 repo.ui.debug('rebase onto %s starting from %s\n' % (dest, root))
1549 1551 if emptyrebase:
1550 1552 return None
1551 1553 for rev in sorted(state):
1552 1554 parents = [p for p in repo.changelog.parentrevs(rev) if p != nullrev]
1553 1555 # if all parents of this revision are done, then so is this revision
1554 1556 if parents and all((state.get(p) == p for p in parents)):
1555 1557 state[rev] = rev
1556 1558 return originalwd, destmap, state
1557 1559
1558 1560 def clearrebased(ui, repo, destmap, state, skipped, collapsedas=None,
1559 1561 keepf=False, fm=None):
1560 1562 """dispose of rebased revision at the end of the rebase
1561 1563
1562 1564 If `collapsedas` is not None, the rebase was a collapse whose result if the
1563 1565 `collapsedas` node.
1564 1566
1565 1567 If `keepf` is not True, the rebase has --keep set and no nodes should be
1566 1568 removed (but bookmarks still need to be moved).
1567 1569 """
1568 1570 tonode = repo.changelog.node
1569 1571 replacements = {}
1570 1572 moves = {}
1571 1573 for rev, newrev in sorted(state.items()):
1572 1574 if newrev >= 0 and newrev != rev:
1573 1575 oldnode = tonode(rev)
1574 1576 newnode = collapsedas or tonode(newrev)
1575 1577 moves[oldnode] = newnode
1576 1578 if not keepf:
1577 1579 if rev in skipped:
1578 1580 succs = ()
1579 1581 else:
1580 1582 succs = (newnode,)
1581 1583 replacements[oldnode] = succs
1582 1584 scmutil.cleanupnodes(repo, replacements, 'rebase', moves)
1583 1585 if fm:
1584 1586 hf = fm.hexfunc
1585 1587 fl = fm.formatlist
1586 1588 fd = fm.formatdict
1587 1589 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1588 1590 for oldn, newn in replacements.iteritems()},
1589 1591 key="oldnode", value="newnodes")
1590 1592 fm.data(nodechanges=nodechanges)
1591 1593
1592 1594 def pullrebase(orig, ui, repo, *args, **opts):
1593 1595 'Call rebase after pull if the latter has been invoked with --rebase'
1594 1596 ret = None
1595 1597 if opts.get(r'rebase'):
1596 1598 if ui.configbool('commands', 'rebase.requiredest'):
1597 1599 msg = _('rebase destination required by configuration')
1598 1600 hint = _('use hg pull followed by hg rebase -d DEST')
1599 1601 raise error.Abort(msg, hint=hint)
1600 1602
1601 1603 with repo.wlock(), repo.lock():
1602 1604 if opts.get(r'update'):
1603 1605 del opts[r'update']
1604 1606 ui.debug('--update and --rebase are not compatible, ignoring '
1605 1607 'the update flag\n')
1606 1608
1607 1609 cmdutil.checkunfinished(repo)
1608 1610 cmdutil.bailifchanged(repo, hint=_('cannot pull with rebase: '
1609 1611 'please commit or shelve your changes first'))
1610 1612
1611 1613 revsprepull = len(repo)
1612 1614 origpostincoming = commands.postincoming
1613 1615 def _dummy(*args, **kwargs):
1614 1616 pass
1615 1617 commands.postincoming = _dummy
1616 1618 try:
1617 1619 ret = orig(ui, repo, *args, **opts)
1618 1620 finally:
1619 1621 commands.postincoming = origpostincoming
1620 1622 revspostpull = len(repo)
1621 1623 if revspostpull > revsprepull:
1622 1624 # --rev option from pull conflict with rebase own --rev
1623 1625 # dropping it
1624 1626 if r'rev' in opts:
1625 1627 del opts[r'rev']
1626 1628 # positional argument from pull conflicts with rebase's own
1627 1629 # --source.
1628 1630 if r'source' in opts:
1629 1631 del opts[r'source']
1630 1632 # revsprepull is the len of the repo, not revnum of tip.
1631 1633 destspace = list(repo.changelog.revs(start=revsprepull))
1632 1634 opts[r'_destspace'] = destspace
1633 1635 try:
1634 1636 rebase(ui, repo, **opts)
1635 1637 except error.NoMergeDestAbort:
1636 1638 # we can maybe update instead
1637 1639 rev, _a, _b = destutil.destupdate(repo)
1638 1640 if rev == repo['.'].rev():
1639 1641 ui.status(_('nothing to rebase\n'))
1640 1642 else:
1641 1643 ui.status(_('nothing to rebase - updating instead\n'))
1642 1644 # not passing argument to get the bare update behavior
1643 1645 # with warning and trumpets
1644 1646 commands.update(ui, repo)
1645 1647 else:
1646 1648 if opts.get(r'tool'):
1647 1649 raise error.Abort(_('--tool can only be used with --rebase'))
1648 1650 ret = orig(ui, repo, *args, **opts)
1649 1651
1650 1652 return ret
1651 1653
1652 1654 def _filterobsoleterevs(repo, revs):
1653 1655 """returns a set of the obsolete revisions in revs"""
1654 1656 return set(r for r in revs if repo[r].obsolete())
1655 1657
1656 1658 def _computeobsoletenotrebased(repo, rebaseobsrevs, destmap):
1657 1659 """Return (obsoletenotrebased, obsoletewithoutsuccessorindestination).
1658 1660
1659 1661 `obsoletenotrebased` is a mapping mapping obsolete => successor for all
1660 1662 obsolete nodes to be rebased given in `rebaseobsrevs`.
1661 1663
1662 1664 `obsoletewithoutsuccessorindestination` is a set with obsolete revisions
1663 1665 without a successor in destination.
1664 1666 """
1665 1667 obsoletenotrebased = {}
1666 1668 obsoletewithoutsuccessorindestination = set([])
1667 1669
1668 1670 assert repo.filtername is None
1669 1671 cl = repo.changelog
1670 1672 nodemap = cl.nodemap
1671 1673 for srcrev in rebaseobsrevs:
1672 1674 srcnode = cl.node(srcrev)
1673 1675 destnode = cl.node(destmap[srcrev])
1674 1676 # XXX: more advanced APIs are required to handle split correctly
1675 1677 successors = list(obsutil.allsuccessors(repo.obsstore, [srcnode]))
1676 1678 if len(successors) == 1:
1677 1679 # obsutil.allsuccessors includes node itself. When the list only
1678 1680 # contains one element, it means there are no successors.
1679 1681 obsoletenotrebased[srcrev] = None
1680 1682 else:
1681 1683 for succnode in successors:
1682 1684 if succnode == srcnode or succnode not in nodemap:
1683 1685 continue
1684 1686 if cl.isancestor(succnode, destnode):
1685 1687 obsoletenotrebased[srcrev] = nodemap[succnode]
1686 1688 break
1687 1689 else:
1688 1690 # If 'srcrev' has a successor in rebase set but none in
1689 1691 # destination (which would be catched above), we shall skip it
1690 1692 # and its descendants to avoid divergence.
1691 1693 if any(nodemap[s] in destmap
1692 1694 for s in successors if s != srcnode):
1693 1695 obsoletewithoutsuccessorindestination.add(srcrev)
1694 1696
1695 1697 return obsoletenotrebased, obsoletewithoutsuccessorindestination
1696 1698
1697 1699 def summaryhook(ui, repo):
1698 1700 if not repo.vfs.exists('rebasestate'):
1699 1701 return
1700 1702 try:
1701 1703 rbsrt = rebaseruntime(repo, ui, {})
1702 1704 rbsrt.restorestatus()
1703 1705 state = rbsrt.state
1704 1706 except error.RepoLookupError:
1705 1707 # i18n: column positioning for "hg summary"
1706 1708 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1707 1709 ui.write(msg)
1708 1710 return
1709 1711 numrebased = len([i for i in state.itervalues() if i >= 0])
1710 1712 # i18n: column positioning for "hg summary"
1711 1713 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1712 1714 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1713 1715 ui.label(_('%d remaining'), 'rebase.remaining') %
1714 1716 (len(state) - numrebased)))
1715 1717
1716 1718 def uisetup(ui):
1717 1719 #Replace pull with a decorator to provide --rebase option
1718 1720 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1719 1721 entry[1].append(('', 'rebase', None,
1720 1722 _("rebase working directory to branch head")))
1721 1723 entry[1].append(('t', 'tool', '',
1722 1724 _("specify merge tool for rebase")))
1723 1725 cmdutil.summaryhooks.add('rebase', summaryhook)
1724 1726 cmdutil.unfinishedstates.append(
1725 1727 ['rebasestate', False, False, _('rebase in progress'),
1726 1728 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
1727 1729 cmdutil.afterresolvedstates.append(
1728 1730 ['rebasestate', _('hg rebase --continue')])
General Comments 0
You need to be logged in to leave comments. Login now