comparison hgext/rebase.py @ 34022:af609bb3487f

rebase: change internal format to support destination map A later patch will add multiple destination support. This patch changes internal state and the rebase state file format to support that. But the external interface still only supports single destination. A test was added to make sure rebase still supports legacy state file. The new state file is incompatible with old clients. We had done similar state file format change before: 5eac7ab, 92409f8, and 72412af. The state file is transient, so the impact of incompatibility is limited. Besides, the old client won't support multiple destinations anyway so it does not really make sense to make the file format compatible with them. Differential Revision: https://phab.mercurial-scm.org/D348
author Jun Wu <quark@fb.com>
date Fri, 11 Aug 2017 00:32:19 -0700
parents ba9d5d48bf95
children 5e83a8fe6bc4
comparison
equal deleted inserted replaced
34021:ba9d5d48bf95 34022:af609bb3487f
19 import errno 19 import errno
20 import os 20 import os
21 21
22 from mercurial.i18n import _ 22 from mercurial.i18n import _
23 from mercurial.node import ( 23 from mercurial.node import (
24 hex,
25 nullid, 24 nullid,
26 nullrev, 25 nullrev,
27 short, 26 short,
28 ) 27 )
29 from mercurial import ( 28 from mercurial import (
58 # The following constants are used throughout the rebase module. The ordering of 57 # The following constants are used throughout the rebase module. The ordering of
59 # their values must be maintained. 58 # their values must be maintained.
60 59
61 # Indicates that a revision needs to be rebased 60 # Indicates that a revision needs to be rebased
62 revtodo = -1 61 revtodo = -1
62 revtodostr = '-1'
63 63
64 # legacy revstates no longer needed in current code 64 # legacy revstates no longer needed in current code
65 # -2: nullmerge, -3: revignored, -4: revprecursor, -5: revpruned 65 # -2: nullmerge, -3: revignored, -4: revprecursor, -5: revpruned
66 legacystates = {'-2', '-3', '-4', '-5'} 66 legacystates = {'-2', '-3', '-4', '-5'}
67 67
144 # Mapping between the old revision id and either what is the new rebased 144 # Mapping between the old revision id and either what is the new rebased
145 # revision or what needs to be done with the old revision. The state 145 # revision or what needs to be done with the old revision. The state
146 # dict will be what contains most of the rebase progress state. 146 # dict will be what contains most of the rebase progress state.
147 self.state = {} 147 self.state = {}
148 self.activebookmark = None 148 self.activebookmark = None
149 self.dest = None 149 self.destmap = {}
150 self.skipped = set() 150 self.skipped = set()
151 151
152 self.collapsef = opts.get('collapse', False) 152 self.collapsef = opts.get('collapse', False)
153 self.collapsemsg = cmdutil.logmessage(ui, opts) 153 self.collapsemsg = cmdutil.logmessage(ui, opts)
154 self.date = opts.get('date', None) 154 self.date = opts.get('date', None)
175 self._writestatus(f) 175 self._writestatus(f)
176 176
177 def _writestatus(self, f): 177 def _writestatus(self, f):
178 repo = self.repo.unfiltered() 178 repo = self.repo.unfiltered()
179 f.write(repo[self.originalwd].hex() + '\n') 179 f.write(repo[self.originalwd].hex() + '\n')
180 f.write(repo[self.dest].hex() + '\n') 180 # was "dest". we now write dest per src root below.
181 f.write('\n')
181 f.write(repo[self.external].hex() + '\n') 182 f.write(repo[self.external].hex() + '\n')
182 f.write('%d\n' % int(self.collapsef)) 183 f.write('%d\n' % int(self.collapsef))
183 f.write('%d\n' % int(self.keepf)) 184 f.write('%d\n' % int(self.keepf))
184 f.write('%d\n' % int(self.keepbranchesf)) 185 f.write('%d\n' % int(self.keepbranchesf))
185 f.write('%s\n' % (self.activebookmark or '')) 186 f.write('%s\n' % (self.activebookmark or ''))
187 destmap = self.destmap
186 for d, v in self.state.iteritems(): 188 for d, v in self.state.iteritems():
187 oldrev = repo[d].hex() 189 oldrev = repo[d].hex()
188 if v >= 0: 190 if v >= 0:
189 newrev = repo[v].hex() 191 newrev = repo[v].hex()
190 elif v == revtodo:
191 # To maintain format compatibility, we have to use nullid.
192 # Please do remove this special case when upgrading the format.
193 newrev = hex(nullid)
194 else: 192 else:
195 newrev = v 193 newrev = v
196 f.write("%s:%s\n" % (oldrev, newrev)) 194 destnode = repo[destmap[d]].hex()
195 f.write("%s:%s:%s\n" % (oldrev, newrev, destnode))
197 repo.ui.debug('rebase status stored\n') 196 repo.ui.debug('rebase status stored\n')
198 197
199 def restorestatus(self): 198 def restorestatus(self):
200 """Restore a previously stored status""" 199 """Restore a previously stored status"""
201 repo = self.repo 200 repo = self.repo
202 keepbranches = None 201 keepbranches = None
203 dest = None 202 legacydest = None
204 collapse = False 203 collapse = False
205 external = nullrev 204 external = nullrev
206 activebookmark = None 205 activebookmark = None
207 state = {} 206 state = {}
207 destmap = {}
208 208
209 try: 209 try:
210 f = repo.vfs("rebasestate") 210 f = repo.vfs("rebasestate")
211 for i, l in enumerate(f.read().splitlines()): 211 for i, l in enumerate(f.read().splitlines()):
212 if i == 0: 212 if i == 0:
213 originalwd = repo[l].rev() 213 originalwd = repo[l].rev()
214 elif i == 1: 214 elif i == 1:
215 dest = repo[l].rev() 215 # this line should be empty in newer version. but legacy
216 # clients may still use it
217 if l:
218 legacydest = repo[l].rev()
216 elif i == 2: 219 elif i == 2:
217 external = repo[l].rev() 220 external = repo[l].rev()
218 elif i == 3: 221 elif i == 3:
219 collapse = bool(int(l)) 222 collapse = bool(int(l))
220 elif i == 4: 223 elif i == 4:
225 # line 6 is a recent addition, so for backwards 228 # line 6 is a recent addition, so for backwards
226 # compatibility check that the line doesn't look like the 229 # compatibility check that the line doesn't look like the
227 # oldrev:newrev lines 230 # oldrev:newrev lines
228 activebookmark = l 231 activebookmark = l
229 else: 232 else:
230 oldrev, newrev = l.split(':') 233 args = l.split(':')
234 oldrev = args[0]
235 newrev = args[1]
231 if newrev in legacystates: 236 if newrev in legacystates:
232 continue 237 continue
233 elif newrev == nullid: 238 if len(args) > 2:
239 destnode = args[2]
240 else:
241 destnode = legacydest
242 destmap[repo[oldrev].rev()] = repo[destnode].rev()
243 if newrev in (nullid, revtodostr):
234 state[repo[oldrev].rev()] = revtodo 244 state[repo[oldrev].rev()] = revtodo
235 # Legacy compat special case 245 # Legacy compat special case
236 else: 246 else:
237 state[repo[oldrev].rev()] = repo[newrev].rev() 247 state[repo[oldrev].rev()] = repo[newrev].rev()
238 248
245 raise error.Abort(_('.hg/rebasestate is incomplete')) 255 raise error.Abort(_('.hg/rebasestate is incomplete'))
246 256
247 skipped = set() 257 skipped = set()
248 # recompute the set of skipped revs 258 # recompute the set of skipped revs
249 if not collapse: 259 if not collapse:
250 seen = {dest} 260 seen = set(destmap.values())
251 for old, new in sorted(state.items()): 261 for old, new in sorted(state.items()):
252 if new != revtodo and new in seen: 262 if new != revtodo and new in seen:
253 skipped.add(old) 263 skipped.add(old)
254 seen.add(new) 264 seen.add(new)
255 repo.ui.debug('computed skipped revs: %s\n' % 265 repo.ui.debug('computed skipped revs: %s\n' %
256 (' '.join(str(r) for r in sorted(skipped)) or None)) 266 (' '.join(str(r) for r in sorted(skipped)) or None))
257 repo.ui.debug('rebase status resumed\n') 267 repo.ui.debug('rebase status resumed\n')
258 _setrebasesetvisibility(repo, set(state.keys()) | {originalwd}) 268 _setrebasesetvisibility(repo, set(state.keys()) | {originalwd})
259 269
260 self.originalwd = originalwd 270 self.originalwd = originalwd
261 self.dest = dest 271 self.destmap = destmap
262 self.state = state 272 self.state = state
263 self.skipped = skipped 273 self.skipped = skipped
264 self.collapsef = collapse 274 self.collapsef = collapse
265 self.keepf = keep 275 self.keepf = keep
266 self.keepbranchesf = keepbranches 276 self.keepbranchesf = keepbranches
267 self.external = external 277 self.external = external
268 self.activebookmark = activebookmark 278 self.activebookmark = activebookmark
269 279
270 def _handleskippingobsolete(self, rebaserevs, obsoleterevs, dest): 280 def _handleskippingobsolete(self, obsoleterevs, destmap):
271 """Compute structures necessary for skipping obsolete revisions 281 """Compute structures necessary for skipping obsolete revisions
272 282
273 rebaserevs: iterable of all revisions that are to be rebased
274 obsoleterevs: iterable of all obsolete revisions in rebaseset 283 obsoleterevs: iterable of all obsolete revisions in rebaseset
275 dest: a destination revision for the rebase operation 284 destmap: {srcrev: destrev} destination revisions
276 """ 285 """
277 self.obsoletenotrebased = {} 286 self.obsoletenotrebased = {}
278 if not self.ui.configbool('experimental', 'rebaseskipobsolete', 287 if not self.ui.configbool('experimental', 'rebaseskipobsolete',
279 default=True): 288 default=True):
280 return 289 return
281 obsoleteset = set(obsoleterevs) 290 obsoleteset = set(obsoleterevs)
282 self.obsoletenotrebased = _computeobsoletenotrebased(self.repo, 291 self.obsoletenotrebased = _computeobsoletenotrebased(self.repo,
283 obsoleteset, dest) 292 obsoleteset, destmap)
284 skippedset = set(self.obsoletenotrebased) 293 skippedset = set(self.obsoletenotrebased)
285 _checkobsrebase(self.repo, self.ui, obsoleteset, skippedset) 294 _checkobsrebase(self.repo, self.ui, obsoleteset, skippedset)
286 295
287 def _prepareabortorcontinue(self, isabort): 296 def _prepareabortorcontinue(self, isabort):
288 try: 297 try:
298 else: 307 else:
299 msg = _('cannot continue inconsistent rebase') 308 msg = _('cannot continue inconsistent rebase')
300 hint = _('use "hg rebase --abort" to clear broken state') 309 hint = _('use "hg rebase --abort" to clear broken state')
301 raise error.Abort(msg, hint=hint) 310 raise error.Abort(msg, hint=hint)
302 if isabort: 311 if isabort:
303 return abort(self.repo, self.originalwd, self.dest, 312 return abort(self.repo, self.originalwd, self.destmap,
304 self.state, activebookmark=self.activebookmark) 313 self.state, activebookmark=self.activebookmark)
305 314
306 def _preparenewrebase(self, dest, rebaseset): 315 def _preparenewrebase(self, destmap):
307 if dest is None: 316 if not destmap:
308 return _nothingtorebase() 317 return _nothingtorebase()
309 318
319 rebaseset = destmap.keys()
310 allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt) 320 allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
311 if (not (self.keepf or allowunstable) 321 if (not (self.keepf or allowunstable)
312 and self.repo.revs('first(children(%ld) - %ld)', 322 and self.repo.revs('first(children(%ld) - %ld)',
313 rebaseset, rebaseset)): 323 rebaseset, rebaseset)):
314 raise error.Abort( 324 raise error.Abort(
315 _("can't remove original changesets with" 325 _("can't remove original changesets with"
316 " unrebased descendants"), 326 " unrebased descendants"),
317 hint=_('use --keep to keep original changesets')) 327 hint=_('use --keep to keep original changesets'))
318 328
319 obsrevs = _filterobsoleterevs(self.repo, set(rebaseset)) 329 obsrevs = _filterobsoleterevs(self.repo, rebaseset)
320 self._handleskippingobsolete(rebaseset, obsrevs, dest.rev()) 330 self._handleskippingobsolete(obsrevs, destmap)
321 331
322 result = buildstate(self.repo, dest, rebaseset, self.collapsef, 332 result = buildstate(self.repo, destmap, self.collapsef,
323 self.obsoletenotrebased) 333 self.obsoletenotrebased)
324 334
325 if not result: 335 if not result:
326 # Empty state built, nothing to rebase 336 # Empty state built, nothing to rebase
327 self.ui.status(_('nothing to rebase\n')) 337 self.ui.status(_('nothing to rebase\n'))
331 if not self.keepf and not root.mutable(): 341 if not self.keepf and not root.mutable():
332 raise error.Abort(_("can't rebase public changeset %s") 342 raise error.Abort(_("can't rebase public changeset %s")
333 % root, 343 % root,
334 hint=_("see 'hg help phases' for details")) 344 hint=_("see 'hg help phases' for details"))
335 345
336 (self.originalwd, self.dest, self.state) = result 346 (self.originalwd, self.destmap, self.state) = result
337 if self.collapsef: 347 if self.collapsef:
338 destancestors = self.repo.changelog.ancestors([self.dest], 348 dests = set(self.destmap.values())
349 if len(dests) != 1:
350 raise error.Abort(
351 _('--collapse does not work with multiple destinations'))
352 destrev = next(iter(dests))
353 destancestors = self.repo.changelog.ancestors([destrev],
339 inclusive=True) 354 inclusive=True)
340 self.external = externalparent(self.repo, self.state, destancestors) 355 self.external = externalparent(self.repo, self.state, destancestors)
341 356
342 if dest.closesbranch() and not self.keepbranchesf: 357 for destrev in sorted(set(destmap.values())):
343 self.ui.status(_('reopening closed branch head %s\n') % dest) 358 dest = self.repo[destrev]
359 if dest.closesbranch() and not self.keepbranchesf:
360 self.ui.status(_('reopening closed branch head %s\n') % dest)
344 361
345 def _performrebase(self, tr): 362 def _performrebase(self, tr):
346 repo, ui, opts = self.repo, self.ui, self.opts 363 repo, ui, opts = self.repo, self.ui, self.opts
347 if self.keepbranchesf: 364 if self.keepbranchesf:
348 # insert _savebranch at the start of extrafns so if 365 # insert _savebranch at the start of extrafns so if
369 sortedrevs = repo.revs('sort(%ld, -topo)', self.state) 386 sortedrevs = repo.revs('sort(%ld, -topo)', self.state)
370 cands = [k for k, v in self.state.iteritems() if v == revtodo] 387 cands = [k for k, v in self.state.iteritems() if v == revtodo]
371 total = len(cands) 388 total = len(cands)
372 pos = 0 389 pos = 0
373 for rev in sortedrevs: 390 for rev in sortedrevs:
391 dest = self.destmap[rev]
374 ctx = repo[rev] 392 ctx = repo[rev]
375 desc = _ctxdesc(ctx) 393 desc = _ctxdesc(ctx)
376 if self.state[rev] == rev: 394 if self.state[rev] == rev:
377 ui.status(_('already rebased %s\n') % desc) 395 ui.status(_('already rebased %s\n') % desc)
378 elif self.state[rev] == revtodo: 396 elif self.state[rev] == revtodo:
379 pos += 1 397 pos += 1
380 ui.status(_('rebasing %s\n') % desc) 398 ui.status(_('rebasing %s\n') % desc)
381 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)), 399 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
382 _('changesets'), total) 400 _('changesets'), total)
383 p1, p2, base = defineparents(repo, rev, self.dest, self.state) 401 p1, p2, base = defineparents(repo, rev, self.destmap,
402 self.state)
384 self.storestatus(tr=tr) 403 self.storestatus(tr=tr)
385 storecollapsemsg(repo, self.collapsemsg) 404 storecollapsemsg(repo, self.collapsemsg)
386 if len(repo[None].parents()) == 2: 405 if len(repo[None].parents()) == 2:
387 repo.ui.debug('resuming interrupted rebase\n') 406 repo.ui.debug('resuming interrupted rebase\n')
388 else: 407 else:
389 try: 408 try:
390 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 409 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
391 'rebase') 410 'rebase')
392 stats = rebasenode(repo, rev, p1, base, self.state, 411 stats = rebasenode(repo, rev, p1, base, self.state,
393 self.collapsef, self.dest) 412 self.collapsef, dest)
394 if stats and stats[3] > 0: 413 if stats and stats[3] > 0:
395 raise error.InterventionRequired( 414 raise error.InterventionRequired(
396 _('unresolved conflicts (see hg ' 415 _('unresolved conflicts (see hg '
397 'resolve, then hg rebase --continue)')) 416 'resolve, then hg rebase --continue)'))
398 finally: 417 finally:
434 ui.note(_('rebase merging completed\n')) 453 ui.note(_('rebase merging completed\n'))
435 454
436 def _finishrebase(self): 455 def _finishrebase(self):
437 repo, ui, opts = self.repo, self.ui, self.opts 456 repo, ui, opts = self.repo, self.ui, self.opts
438 if self.collapsef and not self.keepopen: 457 if self.collapsef and not self.keepopen:
439 p1, p2, _base = defineparents(repo, min(self.state), 458 p1, p2, _base = defineparents(repo, min(self.state), self.destmap,
440 self.dest, self.state) 459 self.state)
441 editopt = opts.get('edit') 460 editopt = opts.get('edit')
442 editform = 'rebase.collapse' 461 editform = 'rebase.collapse'
443 if self.collapsemsg: 462 if self.collapsemsg:
444 commitmsg = self.collapsemsg 463 commitmsg = self.collapsemsg
445 else: 464 else:
481 500
482 if not self.keepf: 501 if not self.keepf:
483 collapsedas = None 502 collapsedas = None
484 if self.collapsef: 503 if self.collapsef:
485 collapsedas = newnode 504 collapsedas = newnode
486 clearrebased(ui, repo, self.dest, self.state, self.skipped, 505 clearrebased(ui, repo, self.destmap, self.state, self.skipped,
487 collapsedas) 506 collapsedas)
488 507
489 clearstatus(repo) 508 clearstatus(repo)
490 clearcollapsemsg(repo) 509 clearcollapsemsg(repo)
491 510
673 692
674 retcode = rbsrt._prepareabortorcontinue(abortf) 693 retcode = rbsrt._prepareabortorcontinue(abortf)
675 if retcode is not None: 694 if retcode is not None:
676 return retcode 695 return retcode
677 else: 696 else:
678 dest, rebaseset = _definesets(ui, repo, destf, srcf, basef, revf, 697 destmap = _definedestmap(ui, repo, destf, srcf, basef, revf,
679 destspace=destspace) 698 destspace=destspace)
680 retcode = rbsrt._preparenewrebase(dest, rebaseset) 699 retcode = rbsrt._preparenewrebase(destmap)
681 if retcode is not None: 700 if retcode is not None:
682 return retcode 701 return retcode
683 702
684 tr = None 703 tr = None
685 dsguard = None 704 dsguard = None
693 with util.acceptintervention(dsguard): 712 with util.acceptintervention(dsguard):
694 rbsrt._performrebase(tr) 713 rbsrt._performrebase(tr)
695 714
696 rbsrt._finishrebase() 715 rbsrt._finishrebase()
697 716
698 def _definesets(ui, repo, destf=None, srcf=None, basef=None, revf=None, 717 def _definedestmap(ui, repo, destf=None, srcf=None, basef=None, revf=None,
699 destspace=None): 718 destspace=None):
700 """use revisions argument to define destination and rebase set 719 """use revisions argument to define destmap {srcrev: destrev}"""
701 """
702 if revf is None: 720 if revf is None:
703 revf = [] 721 revf = []
704 722
705 # destspace is here to work around issues with `hg pull --rebase` see 723 # destspace is here to work around issues with `hg pull --rebase` see
706 # issue5214 for details 724 # issue5214 for details
723 741
724 if revf: 742 if revf:
725 rebaseset = scmutil.revrange(repo, revf) 743 rebaseset = scmutil.revrange(repo, revf)
726 if not rebaseset: 744 if not rebaseset:
727 ui.status(_('empty "rev" revision set - nothing to rebase\n')) 745 ui.status(_('empty "rev" revision set - nothing to rebase\n'))
728 return None, None 746 return None
729 elif srcf: 747 elif srcf:
730 src = scmutil.revrange(repo, [srcf]) 748 src = scmutil.revrange(repo, [srcf])
731 if not src: 749 if not src:
732 ui.status(_('empty "source" revision set - nothing to rebase\n')) 750 ui.status(_('empty "source" revision set - nothing to rebase\n'))
733 return None, None 751 return None
734 rebaseset = repo.revs('(%ld)::', src) 752 rebaseset = repo.revs('(%ld)::', src)
735 assert rebaseset 753 assert rebaseset
736 else: 754 else:
737 base = scmutil.revrange(repo, [basef or '.']) 755 base = scmutil.revrange(repo, [basef or '.'])
738 if not base: 756 if not base:
739 ui.status(_('empty "base" revision set - ' 757 ui.status(_('empty "base" revision set - '
740 "can't compute rebase set\n")) 758 "can't compute rebase set\n"))
741 return None, None 759 return None
742 if not destf: 760 if not destf:
743 dest = repo[_destrebase(repo, base, destspace=destspace)] 761 dest = repo[_destrebase(repo, base, destspace=destspace)]
744 destf = str(dest) 762 destf = str(dest)
745 763
746 roots = [] # selected children of branching points 764 roots = [] # selected children of branching points
780 'directory parent is already an ' 798 'directory parent is already an '
781 'ancestor of destination %s\n') % dest) 799 'ancestor of destination %s\n') % dest)
782 else: # can it happen? 800 else: # can it happen?
783 ui.status(_('nothing to rebase from %s to %s\n') % 801 ui.status(_('nothing to rebase from %s to %s\n') %
784 ('+'.join(str(repo[r]) for r in base), dest)) 802 ('+'.join(str(repo[r]) for r in base), dest))
785 return None, None 803 return None
786 804
787 if not destf: 805 if not destf:
788 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)] 806 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
789 destf = str(dest) 807 destf = str(dest)
790 808
791 return dest, rebaseset 809 # assign dest to each rev in rebaseset
810 destrev = dest.rev()
811 destmap = {r: destrev for r in rebaseset} # {srcrev: destrev}
812
813 return destmap
792 814
793 def externalparent(repo, state, destancestors): 815 def externalparent(repo, state, destancestors):
794 """Return the revision that should be used as the second parent 816 """Return the revision that should be used as the second parent
795 when the revisions in state is collapsed on top of destancestors. 817 when the revisions in state is collapsed on top of destancestors.
796 Abort if there is more than one parent. 818 Abort if there is more than one parent.
872 # performed in the destination. 894 # performed in the destination.
873 p1rev = repo[rev].p1().rev() 895 p1rev = repo[rev].p1().rev()
874 copies.duplicatecopies(repo, rev, p1rev, skiprev=dest) 896 copies.duplicatecopies(repo, rev, p1rev, skiprev=dest)
875 return stats 897 return stats
876 898
877 def adjustdest(repo, rev, dest, state): 899 def adjustdest(repo, rev, destmap, state):
878 """adjust rebase destination given the current rebase state 900 """adjust rebase destination given the current rebase state
879 901
880 rev is what is being rebased. Return a list of two revs, which are the 902 rev is what is being rebased. Return a list of two revs, which are the
881 adjusted destinations for rev's p1 and p2, respectively. If a parent is 903 adjusted destinations for rev's p1 and p2, respectively. If a parent is
882 nullrev, return dest without adjustment for it. 904 nullrev, return dest without adjustment for it.
912 | |/ | 934 | |/ |
913 | B | ... 935 | B | ...
914 |/ |/ 936 |/ |/
915 A A 937 A A
916 """ 938 """
917 # pick already rebased revs from state 939 # pick already rebased revs with same dest from state as interesting source
918 source = [s for s, d in state.items() if d > 0] 940 dest = destmap[rev]
941 source = [s for s, d in state.items() if d > 0 and destmap[s] == dest]
919 942
920 result = [] 943 result = []
921 for prev in repo.changelog.parentrevs(rev): 944 for prev in repo.changelog.parentrevs(rev):
922 adjusted = dest 945 adjusted = dest
923 if prev != nullrev: 946 if prev != nullrev:
955 nodemap = unfi.changelog.nodemap 978 nodemap = unfi.changelog.nodemap
956 for s in obsutil.allsuccessors(unfi.obsstore, [unfi[rev].node()]): 979 for s in obsutil.allsuccessors(unfi.obsstore, [unfi[rev].node()]):
957 if s in nodemap: 980 if s in nodemap:
958 yield nodemap[s] 981 yield nodemap[s]
959 982
960 def defineparents(repo, rev, dest, state): 983 def defineparents(repo, rev, destmap, state):
961 """Return new parents and optionally a merge base for rev being rebased 984 """Return new parents and optionally a merge base for rev being rebased
962 985
963 The destination specified by "dest" cannot always be used directly because 986 The destination specified by "dest" cannot always be used directly because
964 previously rebase result could affect destination. For example, 987 previously rebase result could affect destination. For example,
965 988
979 return True 1002 return True
980 elif a > b: 1003 elif a > b:
981 return False 1004 return False
982 return cl.isancestor(cl.node(a), cl.node(b)) 1005 return cl.isancestor(cl.node(a), cl.node(b))
983 1006
1007 dest = destmap[rev]
984 oldps = repo.changelog.parentrevs(rev) # old parents 1008 oldps = repo.changelog.parentrevs(rev) # old parents
985 newps = [nullrev, nullrev] # new parents 1009 newps = [nullrev, nullrev] # new parents
986 dests = adjustdest(repo, rev, dest, state) # adjusted destinations 1010 dests = adjustdest(repo, rev, destmap, state) # adjusted destinations
987 bases = list(oldps) # merge base candidates, initially just old parents 1011 bases = list(oldps) # merge base candidates, initially just old parents
988 1012
989 if all(r == nullrev for r in oldps[1:]): 1013 if all(r == nullrev for r in oldps[1:]):
990 # For non-merge changeset, just move p to adjusted dest as requested. 1014 # For non-merge changeset, just move p to adjusted dest as requested.
991 newps[0] = dests[0] 1015 newps[0] = dests[0]
1236 if firstunrebased in parents: 1260 if firstunrebased in parents:
1237 return True 1261 return True
1238 1262
1239 return False 1263 return False
1240 1264
1241 def abort(repo, originalwd, dest, state, activebookmark=None): 1265 def abort(repo, originalwd, destmap, state, activebookmark=None):
1242 '''Restore the repository to its original state. Additional args: 1266 '''Restore the repository to its original state. Additional args:
1243 1267
1244 activebookmark: the name of the bookmark that should be active after the 1268 activebookmark: the name of the bookmark that should be active after the
1245 restore''' 1269 restore'''
1246 1270
1247 try: 1271 try:
1248 # If the first commits in the rebased set get skipped during the rebase, 1272 # If the first commits in the rebased set get skipped during the rebase,
1249 # their values within the state mapping will be the dest rev id. The 1273 # their values within the state mapping will be the dest rev id. The
1250 # dstates list must must not contain the dest rev (issue4896) 1274 # dstates list must must not contain the dest rev (issue4896)
1251 dstates = [s for s in state.values() if s >= 0 and s != dest] 1275 dstates = [s for r, s in state.items() if s >= 0 and s != destmap[r]]
1252 immutable = [d for d in dstates if not repo[d].mutable()] 1276 immutable = [d for d in dstates if not repo[d].mutable()]
1253 cleanup = True 1277 cleanup = True
1254 if immutable: 1278 if immutable:
1255 repo.ui.warn(_("warning: can't clean up public changesets %s\n") 1279 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
1256 % ', '.join(str(repo[r]) for r in immutable), 1280 % ', '.join(str(repo[r]) for r in immutable),
1265 "branch, can't strip\n")) 1289 "branch, can't strip\n"))
1266 cleanup = False 1290 cleanup = False
1267 1291
1268 if cleanup: 1292 if cleanup:
1269 shouldupdate = False 1293 shouldupdate = False
1270 rebased = filter(lambda x: x >= 0 and x != dest, state.values()) 1294 rebased = [s for r, s in state.items()
1295 if s >= 0 and s != destmap[r]]
1271 if rebased: 1296 if rebased:
1272 strippoints = [ 1297 strippoints = [
1273 c.node() for c in repo.set('roots(%ld)', rebased)] 1298 c.node() for c in repo.set('roots(%ld)', rebased)]
1274 1299
1275 updateifonnodes = set(rebased) 1300 updateifonnodes = set(rebased)
1276 updateifonnodes.add(dest) 1301 updateifonnodes.update(destmap.values())
1277 updateifonnodes.add(originalwd) 1302 updateifonnodes.add(originalwd)
1278 shouldupdate = repo['.'].rev() in updateifonnodes 1303 shouldupdate = repo['.'].rev() in updateifonnodes
1279 1304
1280 # Update away from the rebase if necessary 1305 # Update away from the rebase if necessary
1281 if shouldupdate or needupdate(repo, state): 1306 if shouldupdate or needupdate(repo, state):
1293 clearstatus(repo) 1318 clearstatus(repo)
1294 clearcollapsemsg(repo) 1319 clearcollapsemsg(repo)
1295 repo.ui.warn(_('rebase aborted\n')) 1320 repo.ui.warn(_('rebase aborted\n'))
1296 return 0 1321 return 0
1297 1322
1298 def buildstate(repo, dest, rebaseset, collapse, obsoletenotrebased): 1323 def buildstate(repo, destmap, collapse, obsoletenotrebased):
1299 '''Define which revisions are going to be rebased and where 1324 '''Define which revisions are going to be rebased and where
1300 1325
1301 repo: repo 1326 repo: repo
1302 dest: context 1327 destmap: {srcrev: destrev}
1303 rebaseset: set of rev
1304 ''' 1328 '''
1329 rebaseset = destmap.keys()
1305 originalwd = repo['.'].rev() 1330 originalwd = repo['.'].rev()
1306 _setrebasesetvisibility(repo, set(rebaseset) | {originalwd}) 1331 _setrebasesetvisibility(repo, set(rebaseset) | {originalwd})
1307 1332
1308 # This check isn't strictly necessary, since mq detects commits over an 1333 # This check isn't strictly necessary, since mq detects commits over an
1309 # applied patch. But it prevents messing up the working directory when 1334 # applied patch. But it prevents messing up the working directory when
1310 # a partially completed rebase is blocked by mq. 1335 # a partially completed rebase is blocked by mq.
1311 if 'qtip' in repo.tags() and (dest.node() in 1336 if 'qtip' in repo.tags():
1312 [s.node for s in repo.mq.applied]): 1337 mqapplied = set(repo[s.node].rev() for s in repo.mq.applied)
1313 raise error.Abort(_('cannot rebase onto an applied mq patch')) 1338 if set(destmap.values()) & mqapplied:
1339 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1314 1340
1315 roots = list(repo.set('roots(%ld)', rebaseset)) 1341 roots = list(repo.set('roots(%ld)', rebaseset))
1316 if not roots: 1342 if not roots:
1317 raise error.Abort(_('no matching revisions')) 1343 raise error.Abort(_('no matching revisions'))
1318 roots.sort() 1344 roots.sort()
1319 state = dict.fromkeys(rebaseset, revtodo) 1345 state = dict.fromkeys(rebaseset, revtodo)
1320 emptyrebase = True 1346 emptyrebase = True
1321 for root in roots: 1347 for root in roots:
1348 dest = repo[destmap[root.rev()]]
1322 commonbase = root.ancestor(dest) 1349 commonbase = root.ancestor(dest)
1323 if commonbase == root: 1350 if commonbase == root:
1324 raise error.Abort(_('source is ancestor of destination')) 1351 raise error.Abort(_('source is ancestor of destination'))
1325 if commonbase == dest: 1352 if commonbase == dest:
1326 wctx = repo[None] 1353 wctx = repo[None]
1350 desc = _ctxdesc(unfi[r]) 1377 desc = _ctxdesc(unfi[r])
1351 succ = obsoletenotrebased[r] 1378 succ = obsoletenotrebased[r]
1352 if succ is None: 1379 if succ is None:
1353 msg = _('note: not rebasing %s, it has no successor\n') % desc 1380 msg = _('note: not rebasing %s, it has no successor\n') % desc
1354 del state[r] 1381 del state[r]
1382 del destmap[r]
1355 else: 1383 else:
1356 destctx = unfi[succ] 1384 destctx = unfi[succ]
1357 destdesc = '%d:%s "%s"' % (destctx.rev(), destctx, 1385 destdesc = '%d:%s "%s"' % (destctx.rev(), destctx,
1358 destctx.description().split('\n', 1)[0]) 1386 destctx.description().split('\n', 1)[0])
1359 msg = (_('note: not rebasing %s, already in destination as %s\n') 1387 msg = (_('note: not rebasing %s, already in destination as %s\n')
1360 % (desc, destdesc)) 1388 % (desc, destdesc))
1361 del state[r] 1389 del state[r]
1390 del destmap[r]
1362 repo.ui.status(msg) 1391 repo.ui.status(msg)
1363 return originalwd, dest.rev(), state 1392 return originalwd, destmap, state
1364 1393
1365 def clearrebased(ui, repo, dest, state, skipped, collapsedas=None): 1394 def clearrebased(ui, repo, destmap, state, skipped, collapsedas=None):
1366 """dispose of rebased revision at the end of the rebase 1395 """dispose of rebased revision at the end of the rebase
1367 1396
1368 If `collapsedas` is not None, the rebase was a collapse whose result if the 1397 If `collapsedas` is not None, the rebase was a collapse whose result if the
1369 `collapsedas` node.""" 1398 `collapsedas` node."""
1370 tonode = repo.changelog.node 1399 tonode = repo.changelog.node
1371 # Move bookmark of skipped nodes to destination. This cannot be handled 1400 # Move bookmark of skipped nodes to destination. This cannot be handled
1372 # by scmutil.cleanupnodes since it will treat rev as removed (no successor) 1401 # by scmutil.cleanupnodes since it will treat rev as removed (no successor)
1373 # and move bookmark backwards. 1402 # and move bookmark backwards.
1374 bmchanges = [(name, tonode(max(adjustdest(repo, rev, dest, state)))) 1403 bmchanges = [(name, tonode(max(adjustdest(repo, rev, destmap, state))))
1375 for rev in skipped 1404 for rev in skipped
1376 for name in repo.nodebookmarks(tonode(rev))] 1405 for name in repo.nodebookmarks(tonode(rev))]
1377 if bmchanges: 1406 if bmchanges:
1378 with repo.transaction('rebase') as tr: 1407 with repo.transaction('rebase') as tr:
1379 repo._bookmarks.applychanges(repo, tr, bmchanges) 1408 repo._bookmarks.applychanges(repo, tr, bmchanges)
1475 1504
1476 def _filterobsoleterevs(repo, revs): 1505 def _filterobsoleterevs(repo, revs):
1477 """returns a set of the obsolete revisions in revs""" 1506 """returns a set of the obsolete revisions in revs"""
1478 return set(r for r in revs if repo[r].obsolete()) 1507 return set(r for r in revs if repo[r].obsolete())
1479 1508
1480 def _computeobsoletenotrebased(repo, rebaseobsrevs, dest): 1509 def _computeobsoletenotrebased(repo, rebaseobsrevs, destmap):
1481 """return a mapping obsolete => successor for all obsolete nodes to be 1510 """return a mapping obsolete => successor for all obsolete nodes to be
1482 rebased that have a successors in the destination 1511 rebased that have a successors in the destination
1483 1512
1484 obsolete => None entries in the mapping indicate nodes with no successor""" 1513 obsolete => None entries in the mapping indicate nodes with no successor"""
1485 obsoletenotrebased = {} 1514 obsoletenotrebased = {}
1486 1515
1487 cl = repo.unfiltered().changelog 1516 cl = repo.unfiltered().changelog
1488 nodemap = cl.nodemap 1517 nodemap = cl.nodemap
1489 destnode = cl.node(dest)
1490 for srcrev in rebaseobsrevs: 1518 for srcrev in rebaseobsrevs:
1491 srcnode = cl.node(srcrev) 1519 srcnode = cl.node(srcrev)
1520 destnode = cl.node(destmap[srcrev])
1492 # XXX: more advanced APIs are required to handle split correctly 1521 # XXX: more advanced APIs are required to handle split correctly
1493 successors = list(obsutil.allsuccessors(repo.obsstore, [srcnode])) 1522 successors = list(obsutil.allsuccessors(repo.obsstore, [srcnode]))
1494 if len(successors) == 1: 1523 if len(successors) == 1:
1495 # obsutil.allsuccessors includes node itself. When the list only 1524 # obsutil.allsuccessors includes node itself. When the list only
1496 # contains one element, it means there are no successors. 1525 # contains one element, it means there are no successors.