Mercurial > hg-stable
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. |