##// END OF EJS Templates
rebase: stop using the `pycompat.open()` shim
Matt Harbison -
r53283:6856d884 default
parent child Browse files
Show More
@@ -1,2281 +1,2280
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 annotations
18 18
19 19 import os
20 20
21 21 from mercurial.i18n import _
22 22 from mercurial.node import (
23 23 nullrev,
24 24 short,
25 25 wdirrev,
26 26 )
27 from mercurial.pycompat import open
28 27 from mercurial import (
29 28 bookmarks,
30 29 cmdutil,
31 30 commands,
32 31 copies,
33 32 destutil,
34 33 error,
35 34 extensions,
36 35 logcmdutil,
37 36 merge as mergemod,
38 37 mergestate as mergestatemod,
39 38 mergeutil,
40 39 obsolete,
41 40 obsutil,
42 41 patch,
43 42 phases,
44 43 pycompat,
45 44 registrar,
46 45 repair,
47 46 revset,
48 47 revsetlang,
49 48 rewriteutil,
50 49 scmutil,
51 50 smartset,
52 51 state as statemod,
53 52 util,
54 53 )
55 54
56 55
57 56 # The following constants are used throughout the rebase module. The ordering of
58 57 # their values must be maintained.
59 58
60 59 # Indicates that a revision needs to be rebased
61 60 revtodo = -1
62 61 revtodostr = b'-1'
63 62
64 63 # legacy revstates no longer needed in current code
65 64 # -2: nullmerge, -3: revignored, -4: revprecursor, -5: revpruned
66 65 legacystates = {b'-2', b'-3', b'-4', b'-5'}
67 66
68 67 cmdtable = {}
69 68 command = registrar.command(cmdtable)
70 69
71 70 configtable = {}
72 71 configitem = registrar.configitem(configtable)
73 72 configitem(
74 73 b'devel',
75 74 b'rebase.force-in-memory-merge',
76 75 default=False,
77 76 )
78 77 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
79 78 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
80 79 # be specifying the version(s) of Mercurial they are tested with, or
81 80 # leave the attribute unspecified.
82 81 testedwith = b'ships-with-hg-core'
83 82
84 83
85 84 def _nothingtorebase():
86 85 return 1
87 86
88 87
89 88 def _savebranch(ctx, extra):
90 89 extra[b'branch'] = ctx.branch()
91 90
92 91
93 92 def _destrebase(repo, sourceset, destspace=None):
94 93 """small wrapper around destmerge to pass the right extra args
95 94
96 95 Please wrap destutil.destmerge instead."""
97 96 return destutil.destmerge(
98 97 repo,
99 98 action=b'rebase',
100 99 sourceset=sourceset,
101 100 onheadcheck=False,
102 101 destspace=destspace,
103 102 )
104 103
105 104
106 105 revsetpredicate = registrar.revsetpredicate()
107 106
108 107
109 108 @revsetpredicate(b'_destrebase')
110 109 def _revsetdestrebase(repo, subset, x):
111 110 # ``_rebasedefaultdest()``
112 111
113 112 # default destination for rebase.
114 113 # # XXX: Currently private because I expect the signature to change.
115 114 # # XXX: - bailing out in case of ambiguity vs returning all data.
116 115 # i18n: "_rebasedefaultdest" is a keyword
117 116 sourceset = None
118 117 if x is not None:
119 118 sourceset = revset.getset(repo, smartset.fullreposet(repo), x)
120 119 return subset & smartset.baseset([_destrebase(repo, sourceset)])
121 120
122 121
123 122 @revsetpredicate(b'_destautoorphanrebase')
124 123 def _revsetdestautoorphanrebase(repo, subset, x):
125 124 # ``_destautoorphanrebase()``
126 125
127 126 # automatic rebase destination for a single orphan revision.
128 127 unfi = repo.unfiltered()
129 128 obsoleted = unfi.revs(b'obsolete()')
130 129
131 130 src = revset.getset(repo, subset, x).first()
132 131
133 132 # Empty src or already obsoleted - Do not return a destination
134 133 if not src or src in obsoleted:
135 134 return smartset.baseset()
136 135 dests = destutil.orphanpossibledestination(repo, src)
137 136 if len(dests) > 1:
138 137 raise error.StateError(
139 138 _(b"ambiguous automatic rebase: %r could end up on any of %r")
140 139 % (src, dests)
141 140 )
142 141 # We have zero or one destination, so we can just return here.
143 142 return smartset.baseset(dests)
144 143
145 144
146 145 def _ctxdesc(ctx):
147 146 """short description for a context"""
148 147 return cmdutil.format_changeset_summary(
149 148 ctx.repo().ui, ctx, command=b'rebase'
150 149 )
151 150
152 151
153 152 class rebaseruntime:
154 153 """This class is a container for rebase runtime state"""
155 154
156 155 def __init__(self, repo, ui, inmemory=False, dryrun=False, opts=None):
157 156 if opts is None:
158 157 opts = {}
159 158
160 159 # prepared: whether we have rebasestate prepared or not. Currently it
161 160 # decides whether "self.repo" is unfiltered or not.
162 161 # The rebasestate has explicit hash to hash instructions not depending
163 162 # on visibility. If rebasestate exists (in-memory or on-disk), use
164 163 # unfiltered repo to avoid visibility issues.
165 164 # Before knowing rebasestate (i.e. when starting a new rebase (not
166 165 # --continue or --abort)), the original repo should be used so
167 166 # visibility-dependent revsets are correct.
168 167 self.prepared = False
169 168 self.resume = False
170 169 self._repo = repo
171 170
172 171 self.ui = ui
173 172 self.opts = opts
174 173 self.originalwd = None
175 174 self.external = nullrev
176 175 # Mapping between the old revision id and either what is the new rebased
177 176 # revision or what needs to be done with the old revision. The state
178 177 # dict will be what contains most of the rebase progress state.
179 178 self.state = {}
180 179 self.activebookmark = None
181 180 self.destmap = {}
182 181 self.skipped = set()
183 182
184 183 self.collapsef = opts.get('collapse', False)
185 184 self.collapsemsg = cmdutil.logmessage(ui, pycompat.byteskwargs(opts))
186 185 self.date = opts.get('date', None)
187 186
188 187 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
189 188 self.extrafns = [rewriteutil.preserve_extras_on_rebase]
190 189 if e:
191 190 self.extrafns = [e]
192 191
193 192 self.backupf = ui.configbool(b'rewrite', b'backup-bundle')
194 193 self.keepf = opts.get('keep', False)
195 194 self.keepbranchesf = opts.get('keepbranches', False)
196 195 self.skipemptysuccessorf = rewriteutil.skip_empty_successor(
197 196 repo.ui, b'rebase'
198 197 )
199 198 self.obsolete_with_successor_in_destination = {}
200 199 self.obsolete_with_successor_in_rebase_set = set()
201 200 self.inmemory = inmemory
202 201 self.dryrun = dryrun
203 202 self.stateobj = statemod.cmdstate(repo, b'rebasestate')
204 203
205 204 @property
206 205 def repo(self):
207 206 if self.prepared:
208 207 return self._repo.unfiltered()
209 208 else:
210 209 return self._repo
211 210
212 211 def storestatus(self, tr=None):
213 212 """Store the current status to allow recovery"""
214 213 if tr:
215 214 tr.addfilegenerator(
216 215 b'rebasestate',
217 216 (b'rebasestate',),
218 217 self._writestatus,
219 218 location=b'plain',
220 219 )
221 220 else:
222 221 with self.repo.vfs(b"rebasestate", b"w") as f:
223 222 self._writestatus(f)
224 223
225 224 def _writestatus(self, f):
226 225 repo = self.repo
227 226 assert repo.filtername is None
228 227 f.write(repo[self.originalwd].hex() + b'\n')
229 228 # was "dest". we now write dest per src root below.
230 229 f.write(b'\n')
231 230 f.write(repo[self.external].hex() + b'\n')
232 231 f.write(b'%d\n' % int(self.collapsef))
233 232 f.write(b'%d\n' % int(self.keepf))
234 233 f.write(b'%d\n' % int(self.keepbranchesf))
235 234 f.write(b'%s\n' % (self.activebookmark or b''))
236 235 destmap = self.destmap
237 236 for d, v in self.state.items():
238 237 oldrev = repo[d].hex()
239 238 if v >= 0:
240 239 newrev = repo[v].hex()
241 240 else:
242 241 newrev = b"%d" % v
243 242 destnode = repo[destmap[d]].hex()
244 243 f.write(b"%s:%s:%s\n" % (oldrev, newrev, destnode))
245 244 repo.ui.debug(b'rebase status stored\n')
246 245
247 246 def restorestatus(self):
248 247 """Restore a previously stored status"""
249 248 if not self.stateobj.exists():
250 249 cmdutil.wrongtooltocontinue(self.repo, _(b'rebase'))
251 250
252 251 data = self._read()
253 252 self.repo.ui.debug(b'rebase status resumed\n')
254 253
255 254 self.originalwd = data[b'originalwd']
256 255 self.destmap = data[b'destmap']
257 256 self.state = data[b'state']
258 257 self.skipped = data[b'skipped']
259 258 self.collapsef = data[b'collapse']
260 259 self.keepf = data[b'keep']
261 260 self.keepbranchesf = data[b'keepbranches']
262 261 self.external = data[b'external']
263 262 self.activebookmark = data[b'activebookmark']
264 263
265 264 def _read(self):
266 265 self.prepared = True
267 266 repo = self.repo
268 267 assert repo.filtername is None
269 268 data = {
270 269 b'keepbranches': None,
271 270 b'collapse': None,
272 271 b'activebookmark': None,
273 272 b'external': nullrev,
274 273 b'keep': None,
275 274 b'originalwd': None,
276 275 }
277 276 legacydest = None
278 277 state = {}
279 278 destmap = {}
280 279
281 280 if True:
282 281 f = repo.vfs(b"rebasestate")
283 282 for i, l in enumerate(f.read().splitlines()):
284 283 if i == 0:
285 284 data[b'originalwd'] = repo[l].rev()
286 285 elif i == 1:
287 286 # this line should be empty in newer version. but legacy
288 287 # clients may still use it
289 288 if l:
290 289 legacydest = repo[l].rev()
291 290 elif i == 2:
292 291 data[b'external'] = repo[l].rev()
293 292 elif i == 3:
294 293 data[b'collapse'] = bool(int(l))
295 294 elif i == 4:
296 295 data[b'keep'] = bool(int(l))
297 296 elif i == 5:
298 297 data[b'keepbranches'] = bool(int(l))
299 298 elif i == 6 and not (len(l) == 81 and b':' in l):
300 299 # line 6 is a recent addition, so for backwards
301 300 # compatibility check that the line doesn't look like the
302 301 # oldrev:newrev lines
303 302 data[b'activebookmark'] = l
304 303 else:
305 304 args = l.split(b':')
306 305 oldrev = repo[args[0]].rev()
307 306 newrev = args[1]
308 307 if newrev in legacystates:
309 308 continue
310 309 if len(args) > 2:
311 310 destrev = repo[args[2]].rev()
312 311 else:
313 312 destrev = legacydest
314 313 destmap[oldrev] = destrev
315 314 if newrev == revtodostr:
316 315 state[oldrev] = revtodo
317 316 # Legacy compat special case
318 317 else:
319 318 state[oldrev] = repo[newrev].rev()
320 319
321 320 if data[b'keepbranches'] is None:
322 321 raise error.Abort(_(b'.hg/rebasestate is incomplete'))
323 322
324 323 data[b'destmap'] = destmap
325 324 data[b'state'] = state
326 325 skipped = set()
327 326 # recompute the set of skipped revs
328 327 if not data[b'collapse']:
329 328 seen = set(destmap.values())
330 329 for old, new in sorted(state.items()):
331 330 if new != revtodo and new in seen:
332 331 skipped.add(old)
333 332 seen.add(new)
334 333 data[b'skipped'] = skipped
335 334 repo.ui.debug(
336 335 b'computed skipped revs: %s\n'
337 336 % (b' '.join(b'%d' % r for r in sorted(skipped)) or b'')
338 337 )
339 338
340 339 return data
341 340
342 341 def _handleskippingobsolete(self):
343 342 """Compute structures necessary for skipping obsolete revisions"""
344 343 if self.keepf:
345 344 return
346 345 if not self.ui.configbool(b'experimental', b'rebaseskipobsolete'):
347 346 return
348 347 obsoleteset = {r for r in self.state if self.repo[r].obsolete()}
349 348 (
350 349 self.obsolete_with_successor_in_destination,
351 350 self.obsolete_with_successor_in_rebase_set,
352 351 ) = _compute_obsolete_sets(self.repo, obsoleteset, self.destmap)
353 352 skippedset = set(self.obsolete_with_successor_in_destination)
354 353 skippedset.update(self.obsolete_with_successor_in_rebase_set)
355 354 _checkobsrebase(self.repo, self.ui, obsoleteset, skippedset)
356 355 if obsolete.isenabled(self.repo, obsolete.allowdivergenceopt):
357 356 self.obsolete_with_successor_in_rebase_set = set()
358 357 else:
359 358 for rev in self.repo.revs(
360 359 b'descendants(%ld) and not %ld',
361 360 self.obsolete_with_successor_in_rebase_set,
362 361 self.obsolete_with_successor_in_rebase_set,
363 362 ):
364 363 self.state.pop(rev, None)
365 364 self.destmap.pop(rev, None)
366 365
367 366 def _prepareabortorcontinue(
368 367 self, isabort, backup=True, suppwarns=False, dryrun=False, confirm=False
369 368 ):
370 369 self.resume = True
371 370 try:
372 371 self.restorestatus()
373 372 # Calculate self.obsolete_* sets
374 373 self._handleskippingobsolete()
375 374 self.collapsemsg = restorecollapsemsg(self.repo, isabort)
376 375 except error.RepoLookupError:
377 376 if isabort:
378 377 clearstatus(self.repo)
379 378 clearcollapsemsg(self.repo)
380 379 self.repo.ui.warn(
381 380 _(
382 381 b'rebase aborted (no revision is removed,'
383 382 b' only broken state is cleared)\n'
384 383 )
385 384 )
386 385 return 0
387 386 else:
388 387 msg = _(b'cannot continue inconsistent rebase')
389 388 hint = _(b'use "hg rebase --abort" to clear broken state')
390 389 raise error.Abort(msg, hint=hint)
391 390
392 391 if isabort:
393 392 backup = backup and self.backupf
394 393 return self._abort(
395 394 backup=backup,
396 395 suppwarns=suppwarns,
397 396 dryrun=dryrun,
398 397 confirm=confirm,
399 398 )
400 399
401 400 def _preparenewrebase(self, destmap):
402 401 if not destmap:
403 402 return _nothingtorebase()
404 403
405 404 result = buildstate(self.repo, destmap, self.collapsef)
406 405
407 406 if not result:
408 407 # Empty state built, nothing to rebase
409 408 self.ui.status(_(b'nothing to rebase\n'))
410 409 return _nothingtorebase()
411 410
412 411 (self.originalwd, self.destmap, self.state) = result
413 412 if self.collapsef:
414 413 dests = set(self.destmap.values())
415 414 if len(dests) != 1:
416 415 raise error.InputError(
417 416 _(b'--collapse does not work with multiple destinations')
418 417 )
419 418 destrev = next(iter(dests))
420 419 destancestors = self.repo.changelog.ancestors(
421 420 [destrev], inclusive=True
422 421 )
423 422 self.external = externalparent(self.repo, self.state, destancestors)
424 423
425 424 for destrev in sorted(set(destmap.values())):
426 425 dest = self.repo[destrev]
427 426 if dest.closesbranch() and not self.keepbranchesf:
428 427 self.ui.status(_(b'reopening closed branch head %s\n') % dest)
429 428
430 429 # Calculate self.obsolete_* sets
431 430 self._handleskippingobsolete()
432 431
433 432 if not self.keepf:
434 433 rebaseset = set(destmap.keys())
435 434 rebaseset -= set(self.obsolete_with_successor_in_destination)
436 435 rebaseset -= self.obsolete_with_successor_in_rebase_set
437 436 # We have our own divergence-checking in the rebase extension
438 437 overrides = {}
439 438 if obsolete.isenabled(self.repo, obsolete.createmarkersopt):
440 439 overrides = {
441 440 (b'experimental', b'evolution.allowdivergence'): b'true'
442 441 }
443 442 try:
444 443 with self.ui.configoverride(overrides):
445 444 rewriteutil.precheck(self.repo, rebaseset, action=b'rebase')
446 445 except error.Abort as e:
447 446 if e.hint is None:
448 447 e.hint = _(b'use --keep to keep original changesets')
449 448 raise e
450 449
451 450 self.prepared = True
452 451
453 452 def _assignworkingcopy(self):
454 453 if self.inmemory:
455 454 from mercurial.context import overlayworkingctx
456 455
457 456 self.wctx = overlayworkingctx(self.repo)
458 457 self.repo.ui.debug(b"rebasing in memory\n")
459 458 else:
460 459 self.wctx = self.repo[None]
461 460 self.repo.ui.debug(b"rebasing on disk\n")
462 461 self.repo.ui.log(
463 462 b"rebase",
464 463 b"using in-memory rebase: %r\n",
465 464 self.inmemory,
466 465 rebase_imm_used=self.inmemory,
467 466 )
468 467
469 468 def _performrebase(self, tr):
470 469 self._assignworkingcopy()
471 470 repo, ui = self.repo, self.ui
472 471 if self.keepbranchesf:
473 472 # insert _savebranch at the start of extrafns so if
474 473 # there's a user-provided extrafn it can clobber branch if
475 474 # desired
476 475 self.extrafns.insert(0, _savebranch)
477 476 if self.collapsef:
478 477 branches = set()
479 478 for rev in self.state:
480 479 branches.add(repo[rev].branch())
481 480 if len(branches) > 1:
482 481 raise error.InputError(
483 482 _(b'cannot collapse multiple named branches')
484 483 )
485 484
486 485 # Keep track of the active bookmarks in order to reset them later
487 486 self.activebookmark = self.activebookmark or repo._activebookmark
488 487 if self.activebookmark:
489 488 bookmarks.deactivate(repo)
490 489
491 490 # Store the state before we begin so users can run 'hg rebase --abort'
492 491 # if we fail before the transaction closes.
493 492 self.storestatus()
494 493 if tr:
495 494 # When using single transaction, store state when transaction
496 495 # commits.
497 496 self.storestatus(tr)
498 497
499 498 cands = [k for k, v in self.state.items() if v == revtodo]
500 499 p = repo.ui.makeprogress(
501 500 _(b"rebasing"), unit=_(b'changesets'), total=len(cands)
502 501 )
503 502
504 503 def progress(ctx):
505 504 p.increment(item=(b"%d:%s" % (ctx.rev(), ctx)))
506 505
507 506 for subset in sortsource(self.destmap):
508 507 sortedrevs = self.repo.revs(b'sort(%ld, -topo)', subset)
509 508 for rev in sortedrevs:
510 509 self._rebasenode(tr, rev, progress)
511 510 p.complete()
512 511 ui.note(_(b'rebase merging completed\n'))
513 512
514 513 def _concludenode(self, rev, editor, commitmsg=None):
515 514 """Commit the wd changes with parents p1 and p2.
516 515
517 516 Reuse commit info from rev but also store useful information in extra.
518 517 Return node of committed revision."""
519 518 repo = self.repo
520 519 ctx = repo[rev]
521 520 if commitmsg is None:
522 521 commitmsg = ctx.description()
523 522
524 523 # Skip replacement if collapsing, as that degenerates to p1 for all
525 524 # nodes.
526 525 if not self.collapsef:
527 526 cl = repo.changelog
528 527 commitmsg = rewriteutil.update_hash_refs(
529 528 repo,
530 529 commitmsg,
531 530 {
532 531 cl.node(oldrev): [cl.node(newrev)]
533 532 for oldrev, newrev in self.state.items()
534 533 if newrev != revtodo
535 534 },
536 535 )
537 536
538 537 date = self.date
539 538 if date is None:
540 539 date = ctx.date()
541 540 extra = {}
542 541 if repo.ui.configbool(b'rebase', b'store-source'):
543 542 extra = {b'rebase_source': ctx.hex()}
544 543 for c in self.extrafns:
545 544 c(ctx, extra)
546 545 destphase = max(ctx.phase(), phases.draft)
547 546 overrides = {
548 547 (b'phases', b'new-commit'): destphase,
549 548 (b'ui', b'allowemptycommit'): not self.skipemptysuccessorf,
550 549 }
551 550 with repo.ui.configoverride(overrides, b'rebase'):
552 551 if self.inmemory:
553 552 newnode = commitmemorynode(
554 553 repo,
555 554 wctx=self.wctx,
556 555 extra=extra,
557 556 commitmsg=commitmsg,
558 557 editor=editor,
559 558 user=ctx.user(),
560 559 date=date,
561 560 )
562 561 else:
563 562 newnode = commitnode(
564 563 repo,
565 564 extra=extra,
566 565 commitmsg=commitmsg,
567 566 editor=editor,
568 567 user=ctx.user(),
569 568 date=date,
570 569 )
571 570
572 571 return newnode
573 572
574 573 def _rebasenode(self, tr, rev, progressfn):
575 574 repo, ui, opts = self.repo, self.ui, self.opts
576 575 ctx = repo[rev]
577 576 desc = _ctxdesc(ctx)
578 577 if self.state[rev] == rev:
579 578 ui.status(_(b'already rebased %s\n') % desc)
580 579 elif rev in self.obsolete_with_successor_in_rebase_set:
581 580 msg = (
582 581 _(
583 582 b'note: not rebasing %s and its descendants as '
584 583 b'this would cause divergence\n'
585 584 )
586 585 % desc
587 586 )
588 587 repo.ui.status(msg)
589 588 self.skipped.add(rev)
590 589 elif rev in self.obsolete_with_successor_in_destination:
591 590 succ = self.obsolete_with_successor_in_destination[rev]
592 591 if succ is None:
593 592 msg = _(b'note: not rebasing %s, it has no successor\n') % desc
594 593 else:
595 594 succdesc = _ctxdesc(repo[succ])
596 595 msg = _(
597 596 b'note: not rebasing %s, already in destination as %s\n'
598 597 ) % (desc, succdesc)
599 598 repo.ui.status(msg)
600 599 # Make clearrebased aware state[rev] is not a true successor
601 600 self.skipped.add(rev)
602 601 # Record rev as moved to its desired destination in self.state.
603 602 # This helps bookmark and working parent movement.
604 603 dest = max(
605 604 adjustdest(repo, rev, self.destmap, self.state, self.skipped)
606 605 )
607 606 self.state[rev] = dest
608 607 elif self.state[rev] == revtodo:
609 608 ui.status(_(b'rebasing %s\n') % desc)
610 609 progressfn(ctx)
611 610 p1, p2, base = defineparents(
612 611 repo,
613 612 rev,
614 613 self.destmap,
615 614 self.state,
616 615 self.skipped,
617 616 self.obsolete_with_successor_in_destination,
618 617 )
619 618 if self.resume and self.wctx.p1().rev() == p1:
620 619 repo.ui.debug(b'resuming interrupted rebase\n')
621 620 self.resume = False
622 621 else:
623 622 overrides = {(b'ui', b'forcemerge'): opts.get('tool', b'')}
624 623 with ui.configoverride(overrides, b'rebase'):
625 624 try:
626 625 rebasenode(
627 626 repo,
628 627 rev,
629 628 p1,
630 629 p2,
631 630 base,
632 631 self.collapsef,
633 632 wctx=self.wctx,
634 633 )
635 634 except error.InMemoryMergeConflictsError:
636 635 if self.dryrun:
637 636 raise error.ConflictResolutionRequired(b'rebase')
638 637 if self.collapsef:
639 638 # TODO: Make the overlayworkingctx reflected
640 639 # in the working copy here instead of re-raising
641 640 # so the entire rebase operation is retried.
642 641 raise
643 642 ui.status(
644 643 _(
645 644 b"hit merge conflicts; rebasing that "
646 645 b"commit again in the working copy\n"
647 646 )
648 647 )
649 648 try:
650 649 cmdutil.bailifchanged(repo)
651 650 except error.Abort:
652 651 clearstatus(repo)
653 652 clearcollapsemsg(repo)
654 653 raise
655 654 self.inmemory = False
656 655 self._assignworkingcopy()
657 656 mergemod.update(repo[p1], wc=self.wctx)
658 657 rebasenode(
659 658 repo,
660 659 rev,
661 660 p1,
662 661 p2,
663 662 base,
664 663 self.collapsef,
665 664 wctx=self.wctx,
666 665 )
667 666 if not self.collapsef:
668 667 merging = p2 != nullrev
669 668 editform = cmdutil.mergeeditform(merging, b'rebase')
670 669 editor = cmdutil.getcommiteditor(editform=editform, **opts)
671 670 # We need to set parents again here just in case we're continuing
672 671 # a rebase started with an old hg version (before 9c9cfecd4600),
673 672 # because those old versions would have left us with two dirstate
674 673 # parents, and we don't want to create a merge commit here (unless
675 674 # we're rebasing a merge commit).
676 675 self.wctx.setparents(repo[p1].node(), repo[p2].node())
677 676 newnode = self._concludenode(rev, editor)
678 677 else:
679 678 # Skip commit if we are collapsing
680 679 newnode = None
681 680 # Update the state
682 681 if newnode is not None:
683 682 self.state[rev] = repo[newnode].rev()
684 683 ui.debug(b'rebased as %s\n' % short(newnode))
685 684 if repo[newnode].isempty():
686 685 ui.warn(
687 686 _(
688 687 b'note: created empty successor for %s, its '
689 688 b'destination already has all its changes\n'
690 689 )
691 690 % desc
692 691 )
693 692 else:
694 693 if not self.collapsef:
695 694 ui.warn(
696 695 _(
697 696 b'note: not rebasing %s, its destination already '
698 697 b'has all its changes\n'
699 698 )
700 699 % desc
701 700 )
702 701 self.skipped.add(rev)
703 702 self.state[rev] = p1
704 703 ui.debug(b'next revision set to %d\n' % p1)
705 704 else:
706 705 ui.status(
707 706 _(b'already rebased %s as %s\n') % (desc, repo[self.state[rev]])
708 707 )
709 708 if not tr:
710 709 # When not using single transaction, store state after each
711 710 # commit is completely done. On InterventionRequired, we thus
712 711 # won't store the status. Instead, we'll hit the "len(parents) == 2"
713 712 # case and realize that the commit was in progress.
714 713 self.storestatus()
715 714
716 715 def _finishrebase(self):
717 716 repo, ui, opts = self.repo, self.ui, self.opts
718 717 fm = ui.formatter(b'rebase', pycompat.byteskwargs(opts))
719 718 fm.startitem()
720 719 if self.collapsef:
721 720 p1, p2, _base = defineparents(
722 721 repo,
723 722 min(self.state),
724 723 self.destmap,
725 724 self.state,
726 725 self.skipped,
727 726 self.obsolete_with_successor_in_destination,
728 727 )
729 728 editopt = opts.get('edit')
730 729 editform = b'rebase.collapse'
731 730 if self.collapsemsg:
732 731 commitmsg = self.collapsemsg
733 732 else:
734 733 commitmsg = b'Collapsed revision'
735 734 for rebased in sorted(self.state):
736 735 if rebased not in self.skipped:
737 736 commitmsg += b'\n* %s' % repo[rebased].description()
738 737 editopt = True
739 738 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
740 739 revtoreuse = max(self.state)
741 740
742 741 self.wctx.setparents(repo[p1].node(), repo[self.external].node())
743 742 newnode = self._concludenode(
744 743 revtoreuse, editor, commitmsg=commitmsg
745 744 )
746 745
747 746 if newnode is not None:
748 747 newrev = repo[newnode].rev()
749 748 for oldrev in self.state:
750 749 self.state[oldrev] = newrev
751 750
752 751 if b'qtip' in repo.tags():
753 752 updatemq(repo, self.state, self.skipped, **opts)
754 753
755 754 # restore original working directory
756 755 # (we do this before stripping)
757 756 newwd = self.state.get(self.originalwd, self.originalwd)
758 757 if newwd < 0:
759 758 # original directory is a parent of rebase set root or ignored
760 759 newwd = self.originalwd
761 760 if newwd not in [c.rev() for c in repo[None].parents()]:
762 761 ui.note(_(b"update back to initial working directory parent\n"))
763 762 mergemod.update(repo[newwd])
764 763
765 764 collapsedas = None
766 765 if self.collapsef and not self.keepf:
767 766 collapsedas = newnode
768 767 clearrebased(
769 768 ui,
770 769 repo,
771 770 self.destmap,
772 771 self.state,
773 772 self.skipped,
774 773 collapsedas,
775 774 self.keepf,
776 775 fm=fm,
777 776 backup=self.backupf,
778 777 )
779 778
780 779 clearstatus(repo)
781 780 clearcollapsemsg(repo)
782 781
783 782 ui.note(_(b"rebase completed\n"))
784 783 util.unlinkpath(repo.sjoin(b'undo'), ignoremissing=True)
785 784 if self.skipped:
786 785 skippedlen = len(self.skipped)
787 786 ui.note(_(b"%d revisions have been skipped\n") % skippedlen)
788 787 fm.end()
789 788
790 789 if (
791 790 self.activebookmark
792 791 and self.activebookmark in repo._bookmarks
793 792 and repo[b'.'].node() == repo._bookmarks[self.activebookmark]
794 793 ):
795 794 bookmarks.activate(repo, self.activebookmark)
796 795
797 796 def _abort(self, backup=True, suppwarns=False, dryrun=False, confirm=False):
798 797 '''Restore the repository to its original state.'''
799 798
800 799 repo = self.repo
801 800 try:
802 801 # If the first commits in the rebased set get skipped during the
803 802 # rebase, their values within the state mapping will be the dest
804 803 # rev id. The rebased list must must not contain the dest rev
805 804 # (issue4896)
806 805 rebased = [
807 806 s
808 807 for r, s in self.state.items()
809 808 if s >= 0 and s != r and s != self.destmap[r]
810 809 ]
811 810 immutable = [d for d in rebased if not repo[d].mutable()]
812 811 cleanup = True
813 812 if immutable:
814 813 repo.ui.warn(
815 814 _(b"warning: can't clean up public changesets %s\n")
816 815 % b', '.join(bytes(repo[r]) for r in immutable),
817 816 hint=_(b"see 'hg help phases' for details"),
818 817 )
819 818 cleanup = False
820 819
821 820 descendants = set()
822 821 if rebased:
823 822 descendants = set(repo.changelog.descendants(rebased))
824 823 if descendants - set(rebased):
825 824 repo.ui.warn(
826 825 _(
827 826 b"warning: new changesets detected on "
828 827 b"destination branch, can't strip\n"
829 828 )
830 829 )
831 830 cleanup = False
832 831
833 832 if cleanup:
834 833 if rebased:
835 834 strippoints = [
836 835 c.node() for c in repo.set(b'roots(%ld)', rebased)
837 836 ]
838 837
839 838 updateifonnodes = set(rebased)
840 839 updateifonnodes.update(self.destmap.values())
841 840
842 841 if not confirm:
843 842 # note: when dry run is set the `rebased` and `destmap`
844 843 # variables seem to contain "bad" contents, so do not
845 844 # rely on them. As dryrun does not need this part of
846 845 # the cleanup, this is "fine"
847 846 updateifonnodes.add(self.originalwd)
848 847
849 848 shouldupdate = repo[b'.'].rev() in updateifonnodes
850 849
851 850 # Update away from the rebase if necessary
852 851 if not dryrun and shouldupdate:
853 852 mergemod.clean_update(repo[self.originalwd])
854 853
855 854 # Strip from the first rebased revision
856 855 if rebased:
857 856 repair.strip(repo.ui, repo, strippoints, backup=backup)
858 857
859 858 if self.activebookmark and self.activebookmark in repo._bookmarks:
860 859 bookmarks.activate(repo, self.activebookmark)
861 860
862 861 finally:
863 862 clearstatus(repo)
864 863 clearcollapsemsg(repo)
865 864 if not suppwarns:
866 865 repo.ui.warn(_(b'rebase aborted\n'))
867 866 return 0
868 867
869 868
870 869 @command(
871 870 b'rebase',
872 871 [
873 872 (
874 873 b's',
875 874 b'source',
876 875 [],
877 876 _(b'rebase the specified changesets and their descendants'),
878 877 _(b'REV'),
879 878 ),
880 879 (
881 880 b'b',
882 881 b'base',
883 882 [],
884 883 _(b'rebase everything from branching point of specified changeset'),
885 884 _(b'REV'),
886 885 ),
887 886 (b'r', b'rev', [], _(b'rebase these revisions'), _(b'REV')),
888 887 (
889 888 b'd',
890 889 b'dest',
891 890 b'',
892 891 _(b'rebase onto the specified changeset'),
893 892 _(b'REV'),
894 893 ),
895 894 (b'', b'collapse', False, _(b'collapse the rebased changesets')),
896 895 (
897 896 b'm',
898 897 b'message',
899 898 b'',
900 899 _(b'use text as collapse commit message'),
901 900 _(b'TEXT'),
902 901 ),
903 902 (b'e', b'edit', False, _(b'invoke editor on commit messages')),
904 903 (
905 904 b'l',
906 905 b'logfile',
907 906 b'',
908 907 _(b'read collapse commit message from file'),
909 908 _(b'FILE'),
910 909 ),
911 910 (b'k', b'keep', False, _(b'keep original changesets')),
912 911 (b'', b'keepbranches', False, _(b'keep original branch names')),
913 912 (b'D', b'detach', False, _(b'(DEPRECATED)')),
914 913 (b'i', b'interactive', False, _(b'(DEPRECATED)')),
915 914 (b't', b'tool', b'', _(b'specify merge tool')),
916 915 (b'', b'stop', False, _(b'stop interrupted rebase')),
917 916 (b'c', b'continue', False, _(b'continue an interrupted rebase')),
918 917 (b'a', b'abort', False, _(b'abort an interrupted rebase')),
919 918 (
920 919 b'',
921 920 b'auto-orphans',
922 921 b'',
923 922 _(
924 923 b'automatically rebase orphan revisions '
925 924 b'in the specified revset (EXPERIMENTAL)'
926 925 ),
927 926 ),
928 927 ]
929 928 + cmdutil.dryrunopts
930 929 + cmdutil.formatteropts
931 930 + cmdutil.confirmopts,
932 931 _(b'[[-s REV]... | [-b REV]... | [-r REV]...] [-d REV] [OPTION]...'),
933 932 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
934 933 )
935 934 def rebase(ui, repo, **opts):
936 935 """move changeset (and descendants) to a different branch
937 936
938 937 Rebase uses repeated merging to graft changesets from one part of
939 938 history (the source) onto another (the destination). This can be
940 939 useful for linearizing *local* changes relative to a master
941 940 development tree.
942 941
943 942 Published commits cannot be rebased (see :hg:`help phases`).
944 943 To copy commits, see :hg:`help graft`.
945 944
946 945 If you don't specify a destination changeset (``-d/--dest``), rebase
947 946 will use the same logic as :hg:`merge` to pick a destination. if
948 947 the current branch contains exactly one other head, the other head
949 948 is merged with by default. Otherwise, an explicit revision with
950 949 which to merge with must be provided. (destination changeset is not
951 950 modified by rebasing, but new changesets are added as its
952 951 descendants.)
953 952
954 953 Here are the ways to select changesets:
955 954
956 955 1. Explicitly select them using ``--rev``.
957 956
958 957 2. Use ``--source`` to select a root changeset and include all of its
959 958 descendants.
960 959
961 960 3. Use ``--base`` to select a changeset; rebase will find ancestors
962 961 and their descendants which are not also ancestors of the destination.
963 962
964 963 4. If you do not specify any of ``--rev``, ``--source``, or ``--base``,
965 964 rebase will use ``--base .`` as above.
966 965
967 966 If ``--source`` or ``--rev`` is used, special names ``SRC`` and ``ALLSRC``
968 967 can be used in ``--dest``. Destination would be calculated per source
969 968 revision with ``SRC`` substituted by that single source revision and
970 969 ``ALLSRC`` substituted by all source revisions.
971 970
972 971 Rebase will destroy original changesets unless you use ``--keep``.
973 972 It will also move your bookmarks (even if you do).
974 973
975 974 Some changesets may be dropped if they do not contribute changes
976 975 (e.g. merges from the destination branch).
977 976
978 977 Unlike ``merge``, rebase will do nothing if you are at the branch tip of
979 978 a named branch with two heads. You will need to explicitly specify source
980 979 and/or destination.
981 980
982 981 If you need to use a tool to automate merge/conflict decisions, you
983 982 can specify one with ``--tool``, see :hg:`help merge-tools`.
984 983 As a caveat: the tool will not be used to mediate when a file was
985 984 deleted, there is no hook presently available for this.
986 985
987 986 If a rebase is interrupted to manually resolve a conflict, it can be
988 987 continued with --continue/-c, aborted with --abort/-a, or stopped with
989 988 --stop.
990 989
991 990 .. container:: verbose
992 991
993 992 Examples:
994 993
995 994 - move "local changes" (current commit back to branching point)
996 995 to the current branch tip after a pull::
997 996
998 997 hg rebase
999 998
1000 999 - move a single changeset to the stable branch::
1001 1000
1002 1001 hg rebase -r 5f493448 -d stable
1003 1002
1004 1003 - splice a commit and all its descendants onto another part of history::
1005 1004
1006 1005 hg rebase --source c0c3 --dest 4cf9
1007 1006
1008 1007 - rebase everything on a branch marked by a bookmark onto the
1009 1008 default branch::
1010 1009
1011 1010 hg rebase --base myfeature --dest default
1012 1011
1013 1012 - collapse a sequence of changes into a single commit::
1014 1013
1015 1014 hg rebase --collapse -r 1520:1525 -d .
1016 1015
1017 1016 - move a named branch while preserving its name::
1018 1017
1019 1018 hg rebase -r "branch(featureX)" -d 1.3 --keepbranches
1020 1019
1021 1020 - stabilize orphaned changesets so history looks linear::
1022 1021
1023 1022 hg rebase -r 'orphan()-obsolete()'\
1024 1023 -d 'first(max((successors(max(roots(ALLSRC) & ::SRC)^)-obsolete())::) +\
1025 1024 max(::((roots(ALLSRC) & ::SRC)^)-obsolete()))'
1026 1025
1027 1026 Configuration Options:
1028 1027
1029 1028 You can make rebase require a destination if you set the following config
1030 1029 option::
1031 1030
1032 1031 [commands]
1033 1032 rebase.requiredest = True
1034 1033
1035 1034 By default, rebase will close the transaction after each commit. For
1036 1035 performance purposes, you can configure rebase to use a single transaction
1037 1036 across the entire rebase. WARNING: This setting introduces a significant
1038 1037 risk of losing the work you've done in a rebase if the rebase aborts
1039 1038 unexpectedly::
1040 1039
1041 1040 [rebase]
1042 1041 singletransaction = True
1043 1042
1044 1043 By default, rebase writes to the working copy, but you can configure it to
1045 1044 run in-memory for better performance. When the rebase is not moving the
1046 1045 parent(s) of the working copy (AKA the "currently checked out changesets"),
1047 1046 this may also allow it to run even if the working copy is dirty::
1048 1047
1049 1048 [rebase]
1050 1049 experimental.inmemory = True
1051 1050
1052 1051 Return Values:
1053 1052
1054 1053 Returns 0 on success, 1 if nothing to rebase or there are
1055 1054 unresolved conflicts.
1056 1055
1057 1056 """
1058 1057 inmemory = ui.configbool(b'rebase', b'experimental.inmemory')
1059 1058 action = cmdutil.check_at_most_one_arg(opts, 'abort', 'stop', 'continue')
1060 1059 if action:
1061 1060 cmdutil.check_incompatible_arguments(
1062 1061 opts, action, ['confirm', 'dry_run']
1063 1062 )
1064 1063 cmdutil.check_incompatible_arguments(
1065 1064 opts, action, ['rev', 'source', 'base', 'dest']
1066 1065 )
1067 1066 cmdutil.check_at_most_one_arg(opts, 'confirm', 'dry_run')
1068 1067 cmdutil.check_at_most_one_arg(opts, 'rev', 'source', 'base')
1069 1068
1070 1069 if action or repo.currenttransaction() is not None:
1071 1070 # in-memory rebase is not compatible with resuming rebases.
1072 1071 # (Or if it is run within a transaction, since the restart logic can
1073 1072 # fail the entire transaction.)
1074 1073 inmemory = False
1075 1074
1076 1075 if opts.get('auto_orphans'):
1077 1076 disallowed_opts = set(opts) - {'auto_orphans'}
1078 1077 cmdutil.check_incompatible_arguments(
1079 1078 opts, 'auto_orphans', disallowed_opts
1080 1079 )
1081 1080
1082 1081 userrevs = list(repo.revs(opts.get('auto_orphans')))
1083 1082 opts['rev'] = [revsetlang.formatspec(b'%ld and orphan()', userrevs)]
1084 1083 opts['dest'] = b'_destautoorphanrebase(SRC)'
1085 1084
1086 1085 if opts.get('dry_run') or opts.get('confirm'):
1087 1086 return _dryrunrebase(ui, repo, action, opts)
1088 1087 elif action == 'stop':
1089 1088 rbsrt = rebaseruntime(repo, ui)
1090 1089 with repo.wlock(), repo.lock():
1091 1090 rbsrt.restorestatus()
1092 1091 if rbsrt.collapsef:
1093 1092 raise error.StateError(_(b"cannot stop in --collapse session"))
1094 1093 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
1095 1094 if not (rbsrt.keepf or allowunstable):
1096 1095 raise error.StateError(
1097 1096 _(
1098 1097 b"cannot remove original changesets with"
1099 1098 b" unrebased descendants"
1100 1099 ),
1101 1100 hint=_(
1102 1101 b'either enable obsmarkers to allow unstable '
1103 1102 b'revisions or use --keep to keep original '
1104 1103 b'changesets'
1105 1104 ),
1106 1105 )
1107 1106 # update to the current working revision
1108 1107 # to clear interrupted merge
1109 1108 mergemod.clean_update(repo[rbsrt.originalwd])
1110 1109 rbsrt._finishrebase()
1111 1110 return 0
1112 1111 elif inmemory:
1113 1112 try:
1114 1113 # in-memory merge doesn't support conflicts, so if we hit any, abort
1115 1114 # and re-run as an on-disk merge.
1116 1115 overrides = {(b'rebase', b'singletransaction'): True}
1117 1116 with ui.configoverride(overrides, b'rebase'):
1118 1117 return _dorebase(ui, repo, action, opts, inmemory=inmemory)
1119 1118 except error.InMemoryMergeConflictsError:
1120 1119 if ui.configbool(b'devel', b'rebase.force-in-memory-merge'):
1121 1120 raise
1122 1121 ui.warn(
1123 1122 _(
1124 1123 b'hit merge conflicts; re-running rebase without in-memory'
1125 1124 b' merge\n'
1126 1125 )
1127 1126 )
1128 1127 clearstatus(repo)
1129 1128 clearcollapsemsg(repo)
1130 1129 return _dorebase(ui, repo, action, opts, inmemory=False)
1131 1130 else:
1132 1131 return _dorebase(ui, repo, action, opts)
1133 1132
1134 1133
1135 1134 def _dryrunrebase(ui, repo, action, opts):
1136 1135 rbsrt = rebaseruntime(repo, ui, inmemory=True, dryrun=True, opts=opts)
1137 1136 confirm = opts.get('confirm')
1138 1137 if confirm:
1139 1138 ui.status(_(b'starting in-memory rebase\n'))
1140 1139 else:
1141 1140 ui.status(
1142 1141 _(b'starting dry-run rebase; repository will not be changed\n')
1143 1142 )
1144 1143 with repo.wlock(), repo.lock():
1145 1144 needsabort = True
1146 1145 try:
1147 1146 overrides = {(b'rebase', b'singletransaction'): True}
1148 1147 with ui.configoverride(overrides, b'rebase'):
1149 1148 res = _origrebase(
1150 1149 ui,
1151 1150 repo,
1152 1151 action,
1153 1152 opts,
1154 1153 rbsrt,
1155 1154 )
1156 1155 if res == _nothingtorebase():
1157 1156 needsabort = False
1158 1157 return res
1159 1158 except error.ConflictResolutionRequired:
1160 1159 ui.status(_(b'hit a merge conflict\n'))
1161 1160 return 1
1162 1161 except error.Abort:
1163 1162 needsabort = False
1164 1163 raise
1165 1164 else:
1166 1165 if confirm:
1167 1166 ui.status(_(b'rebase completed successfully\n'))
1168 1167 if not ui.promptchoice(_(b'apply changes (yn)?$$ &Yes $$ &No')):
1169 1168 # finish unfinished rebase
1170 1169 rbsrt._finishrebase()
1171 1170 else:
1172 1171 rbsrt._prepareabortorcontinue(
1173 1172 isabort=True,
1174 1173 backup=False,
1175 1174 suppwarns=True,
1176 1175 confirm=confirm,
1177 1176 )
1178 1177 needsabort = False
1179 1178 else:
1180 1179 ui.status(
1181 1180 _(
1182 1181 b'dry-run rebase completed successfully; run without'
1183 1182 b' -n/--dry-run to perform this rebase\n'
1184 1183 )
1185 1184 )
1186 1185 return 0
1187 1186 finally:
1188 1187 if needsabort:
1189 1188 # no need to store backup in case of dryrun
1190 1189 rbsrt._prepareabortorcontinue(
1191 1190 isabort=True,
1192 1191 backup=False,
1193 1192 suppwarns=True,
1194 1193 dryrun=opts.get('dry_run'),
1195 1194 )
1196 1195
1197 1196
1198 1197 def _dorebase(ui, repo, action, opts, inmemory=False):
1199 1198 rbsrt = rebaseruntime(repo, ui, inmemory, opts=opts)
1200 1199 return _origrebase(ui, repo, action, opts, rbsrt)
1201 1200
1202 1201
1203 1202 def _origrebase(ui, repo, action, opts, rbsrt):
1204 1203 assert action != 'stop'
1205 1204 with repo.wlock(), repo.lock():
1206 1205 if opts.get('interactive'):
1207 1206 try:
1208 1207 if extensions.find(b'histedit'):
1209 1208 enablehistedit = b''
1210 1209 except KeyError:
1211 1210 enablehistedit = b" --config extensions.histedit="
1212 1211 help = b"hg%s help -e histedit" % enablehistedit
1213 1212 msg = (
1214 1213 _(
1215 1214 b"interactive history editing is supported by the "
1216 1215 b"'histedit' extension (see \"%s\")"
1217 1216 )
1218 1217 % help
1219 1218 )
1220 1219 raise error.InputError(msg)
1221 1220
1222 1221 if rbsrt.collapsemsg and not rbsrt.collapsef:
1223 1222 raise error.InputError(
1224 1223 _(b'message can only be specified with collapse')
1225 1224 )
1226 1225
1227 1226 if action:
1228 1227 if rbsrt.collapsef:
1229 1228 raise error.InputError(
1230 1229 _(b'cannot use collapse with continue or abort')
1231 1230 )
1232 1231 if action == 'abort' and opts.get('tool', False):
1233 1232 ui.warn(_(b'tool option will be ignored\n'))
1234 1233 if action == 'continue':
1235 1234 ms = mergestatemod.mergestate.read(repo)
1236 1235 mergeutil.checkunresolved(ms)
1237 1236
1238 1237 retcode = rbsrt._prepareabortorcontinue(isabort=(action == 'abort'))
1239 1238 if retcode is not None:
1240 1239 return retcode
1241 1240 else:
1242 1241 # search default destination in this space
1243 1242 # used in the 'hg pull --rebase' case, see issue 5214.
1244 1243 destspace = opts.get('_destspace')
1245 1244 destmap = _definedestmap(
1246 1245 ui,
1247 1246 repo,
1248 1247 rbsrt.inmemory,
1249 1248 opts.get('dest', None),
1250 1249 opts.get('source', []),
1251 1250 opts.get('base', []),
1252 1251 opts.get('rev', []),
1253 1252 destspace=destspace,
1254 1253 )
1255 1254 retcode = rbsrt._preparenewrebase(destmap)
1256 1255 if retcode is not None:
1257 1256 return retcode
1258 1257 storecollapsemsg(repo, rbsrt.collapsemsg)
1259 1258
1260 1259 tr = None
1261 1260
1262 1261 singletr = ui.configbool(b'rebase', b'singletransaction')
1263 1262 if singletr:
1264 1263 tr = repo.transaction(b'rebase')
1265 1264
1266 1265 # If `rebase.singletransaction` is enabled, wrap the entire operation in
1267 1266 # one transaction here. Otherwise, transactions are obtained when
1268 1267 # committing each node, which is slower but allows partial success.
1269 1268 with util.acceptintervention(tr):
1270 1269 rbsrt._performrebase(tr)
1271 1270 if not rbsrt.dryrun:
1272 1271 rbsrt._finishrebase()
1273 1272
1274 1273
1275 1274 def _definedestmap(ui, repo, inmemory, destf, srcf, basef, revf, destspace):
1276 1275 """use revisions argument to define destmap {srcrev: destrev}"""
1277 1276 if revf is None:
1278 1277 revf = []
1279 1278
1280 1279 # destspace is here to work around issues with `hg pull --rebase` see
1281 1280 # issue5214 for details
1282 1281
1283 1282 cmdutil.checkunfinished(repo)
1284 1283 if not inmemory:
1285 1284 cmdutil.bailifchanged(repo)
1286 1285
1287 1286 if ui.configbool(b'commands', b'rebase.requiredest') and not destf:
1288 1287 raise error.InputError(
1289 1288 _(b'you must specify a destination'),
1290 1289 hint=_(b'use: hg rebase -d REV'),
1291 1290 )
1292 1291
1293 1292 dest = None
1294 1293
1295 1294 if revf:
1296 1295 rebaseset = logcmdutil.revrange(repo, revf)
1297 1296 if not rebaseset:
1298 1297 ui.status(_(b'empty "rev" revision set - nothing to rebase\n'))
1299 1298 return None
1300 1299 elif srcf:
1301 1300 src = logcmdutil.revrange(repo, srcf)
1302 1301 if not src:
1303 1302 ui.status(_(b'empty "source" revision set - nothing to rebase\n'))
1304 1303 return None
1305 1304 # `+ (%ld)` to work around `wdir()::` being empty
1306 1305 rebaseset = repo.revs(b'(%ld):: + (%ld)', src, src)
1307 1306 else:
1308 1307 base = logcmdutil.revrange(repo, basef or [b'.'])
1309 1308 if not base:
1310 1309 ui.status(
1311 1310 _(b'empty "base" revision set - ' b"can't compute rebase set\n")
1312 1311 )
1313 1312 return None
1314 1313 if destf:
1315 1314 # --base does not support multiple destinations
1316 1315 dest = logcmdutil.revsingle(repo, destf)
1317 1316 else:
1318 1317 dest = repo[_destrebase(repo, base, destspace=destspace)]
1319 1318 destf = bytes(dest)
1320 1319
1321 1320 roots = [] # selected children of branching points
1322 1321 bpbase = {} # {branchingpoint: [origbase]}
1323 1322 for b in base: # group bases by branching points
1324 1323 bp = repo.revs(b'ancestor(%d, %d)', b, dest.rev()).first()
1325 1324 bpbase[bp] = bpbase.get(bp, []) + [b]
1326 1325 if None in bpbase:
1327 1326 # emulate the old behavior, showing "nothing to rebase" (a better
1328 1327 # behavior may be abort with "cannot find branching point" error)
1329 1328 bpbase.clear()
1330 1329 for bp, bs in bpbase.items(): # calculate roots
1331 1330 roots += list(repo.revs(b'children(%d) & ancestors(%ld)', bp, bs))
1332 1331
1333 1332 rebaseset = repo.revs(b'%ld::', roots)
1334 1333
1335 1334 if not rebaseset:
1336 1335 # transform to list because smartsets are not comparable to
1337 1336 # lists. This should be improved to honor laziness of
1338 1337 # smartset.
1339 1338 if list(base) == [dest.rev()]:
1340 1339 if basef:
1341 1340 ui.status(
1342 1341 _(
1343 1342 b'nothing to rebase - %s is both "base"'
1344 1343 b' and destination\n'
1345 1344 )
1346 1345 % dest
1347 1346 )
1348 1347 else:
1349 1348 ui.status(
1350 1349 _(
1351 1350 b'nothing to rebase - working directory '
1352 1351 b'parent is also destination\n'
1353 1352 )
1354 1353 )
1355 1354 elif not repo.revs(b'%ld - ::%d', base, dest.rev()):
1356 1355 if basef:
1357 1356 ui.status(
1358 1357 _(
1359 1358 b'nothing to rebase - "base" %s is '
1360 1359 b'already an ancestor of destination '
1361 1360 b'%s\n'
1362 1361 )
1363 1362 % (b'+'.join(bytes(repo[r]) for r in base), dest)
1364 1363 )
1365 1364 else:
1366 1365 ui.status(
1367 1366 _(
1368 1367 b'nothing to rebase - working '
1369 1368 b'directory parent is already an '
1370 1369 b'ancestor of destination %s\n'
1371 1370 )
1372 1371 % dest
1373 1372 )
1374 1373 else: # can it happen?
1375 1374 ui.status(
1376 1375 _(b'nothing to rebase from %s to %s\n')
1377 1376 % (b'+'.join(bytes(repo[r]) for r in base), dest)
1378 1377 )
1379 1378 return None
1380 1379
1381 1380 if wdirrev in rebaseset:
1382 1381 raise error.InputError(_(b'cannot rebase the working copy'))
1383 1382 rebasingwcp = repo[b'.'].rev() in rebaseset
1384 1383 ui.log(
1385 1384 b"rebase",
1386 1385 b"rebasing working copy parent: %r\n",
1387 1386 rebasingwcp,
1388 1387 rebase_rebasing_wcp=rebasingwcp,
1389 1388 )
1390 1389 if inmemory and rebasingwcp:
1391 1390 # Check these since we did not before.
1392 1391 cmdutil.checkunfinished(repo)
1393 1392 cmdutil.bailifchanged(repo)
1394 1393
1395 1394 if not destf:
1396 1395 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
1397 1396 destf = bytes(dest)
1398 1397
1399 1398 allsrc = revsetlang.formatspec(b'%ld', rebaseset)
1400 1399 alias = {b'ALLSRC': allsrc}
1401 1400
1402 1401 if dest is None:
1403 1402 try:
1404 1403 # fast path: try to resolve dest without SRC alias
1405 1404 dest = scmutil.revsingle(repo, destf, localalias=alias)
1406 1405 except error.RepoLookupError:
1407 1406 # multi-dest path: resolve dest for each SRC separately
1408 1407 destmap = {}
1409 1408 for r in rebaseset:
1410 1409 alias[b'SRC'] = revsetlang.formatspec(b'%d', r)
1411 1410 # use repo.anyrevs instead of scmutil.revsingle because we
1412 1411 # don't want to abort if destset is empty.
1413 1412 destset = repo.anyrevs([destf], user=True, localalias=alias)
1414 1413 size = len(destset)
1415 1414 if size == 1:
1416 1415 destmap[r] = destset.first()
1417 1416 elif size == 0:
1418 1417 ui.note(_(b'skipping %s - empty destination\n') % repo[r])
1419 1418 else:
1420 1419 raise error.InputError(
1421 1420 _(b'rebase destination for %s is not unique') % repo[r]
1422 1421 )
1423 1422
1424 1423 if dest is not None:
1425 1424 # single-dest case: assign dest to each rev in rebaseset
1426 1425 destrev = dest.rev()
1427 1426 destmap = {r: destrev for r in rebaseset} # {srcrev: destrev}
1428 1427
1429 1428 if not destmap:
1430 1429 ui.status(_(b'nothing to rebase - empty destination\n'))
1431 1430 return None
1432 1431
1433 1432 return destmap
1434 1433
1435 1434
1436 1435 def externalparent(repo, state, destancestors):
1437 1436 """Return the revision that should be used as the second parent
1438 1437 when the revisions in state is collapsed on top of destancestors.
1439 1438 Abort if there is more than one parent.
1440 1439 """
1441 1440 parents = set()
1442 1441 source = min(state)
1443 1442 for rev in state:
1444 1443 if rev == source:
1445 1444 continue
1446 1445 for p in repo[rev].parents():
1447 1446 if p.rev() not in state and p.rev() not in destancestors:
1448 1447 parents.add(p.rev())
1449 1448 if not parents:
1450 1449 return nullrev
1451 1450 if len(parents) == 1:
1452 1451 return parents.pop()
1453 1452 raise error.StateError(
1454 1453 _(
1455 1454 b'unable to collapse on top of %d, there is more '
1456 1455 b'than one external parent: %s'
1457 1456 )
1458 1457 % (max(destancestors), b', '.join(b"%d" % p for p in sorted(parents)))
1459 1458 )
1460 1459
1461 1460
1462 1461 def commitmemorynode(repo, wctx, editor, extra, user, date, commitmsg):
1463 1462 """Commit the memory changes with parents p1 and p2.
1464 1463 Return node of committed revision."""
1465 1464 # By convention, ``extra['branch']`` (set by extrafn) clobbers
1466 1465 # ``branch`` (used when passing ``--keepbranches``).
1467 1466 branch = None
1468 1467 if b'branch' in extra:
1469 1468 branch = extra[b'branch']
1470 1469
1471 1470 # FIXME: We call _compact() because it's required to correctly detect
1472 1471 # changed files. This was added to fix a regression shortly before the 5.5
1473 1472 # release. A proper fix will be done in the default branch.
1474 1473 wctx._compact()
1475 1474 memctx = wctx.tomemctx(
1476 1475 commitmsg,
1477 1476 date=date,
1478 1477 extra=extra,
1479 1478 user=user,
1480 1479 branch=branch,
1481 1480 editor=editor,
1482 1481 )
1483 1482 if memctx.isempty() and not repo.ui.configbool(b'ui', b'allowemptycommit'):
1484 1483 return None
1485 1484 commitres = repo.commitctx(memctx)
1486 1485 wctx.clean() # Might be reused
1487 1486 return commitres
1488 1487
1489 1488
1490 1489 def commitnode(repo, editor, extra, user, date, commitmsg):
1491 1490 """Commit the wd changes with parents p1 and p2.
1492 1491 Return node of committed revision."""
1493 1492 tr = util.nullcontextmanager
1494 1493 if not repo.ui.configbool(b'rebase', b'singletransaction'):
1495 1494 tr = lambda: repo.transaction(b'rebase')
1496 1495 with tr():
1497 1496 # Commit might fail if unresolved files exist
1498 1497 newnode = repo.commit(
1499 1498 text=commitmsg, user=user, date=date, extra=extra, editor=editor
1500 1499 )
1501 1500
1502 1501 repo.dirstate.setbranch(
1503 1502 repo[newnode].branch(), repo.currenttransaction()
1504 1503 )
1505 1504 return newnode
1506 1505
1507 1506
1508 1507 def rebasenode(repo, rev, p1, p2, base, collapse, wctx):
1509 1508 """Rebase a single revision rev on top of p1 using base as merge ancestor"""
1510 1509 # Merge phase
1511 1510 # Update to destination and merge it with local
1512 1511 p1ctx = repo[p1]
1513 1512 if wctx.isinmemory():
1514 1513 wctx.setbase(p1ctx)
1515 1514 scope = util.nullcontextmanager
1516 1515 else:
1517 1516 if repo[b'.'].rev() != p1:
1518 1517 repo.ui.debug(b" update to %d:%s\n" % (p1, p1ctx))
1519 1518 mergemod.clean_update(p1ctx)
1520 1519 else:
1521 1520 repo.ui.debug(b" already in destination\n")
1522 1521 scope = lambda: repo.dirstate.changing_parents(repo)
1523 1522 # This is, alas, necessary to invalidate workingctx's manifest cache,
1524 1523 # as well as other data we litter on it in other places.
1525 1524 wctx = repo[None]
1526 1525 repo.dirstate.write(repo.currenttransaction())
1527 1526 ctx = repo[rev]
1528 1527 repo.ui.debug(b" merge against %d:%s\n" % (rev, ctx))
1529 1528 if base is not None:
1530 1529 repo.ui.debug(b" detach base %d:%s\n" % (base, repo[base]))
1531 1530
1532 1531 with scope():
1533 1532 # See explanation in merge.graft()
1534 1533 mergeancestor = repo.changelog.isancestor(p1ctx.node(), ctx.node())
1535 1534 stats = mergemod._update(
1536 1535 repo,
1537 1536 rev,
1538 1537 branchmerge=True,
1539 1538 force=True,
1540 1539 ancestor=base,
1541 1540 mergeancestor=mergeancestor,
1542 1541 labels=[b'dest', b'source', b'parent of source'],
1543 1542 wc=wctx,
1544 1543 )
1545 1544 wctx.setparents(p1ctx.node(), repo[p2].node())
1546 1545 if collapse:
1547 1546 copies.graftcopies(wctx, ctx, p1ctx)
1548 1547 else:
1549 1548 # If we're not using --collapse, we need to
1550 1549 # duplicate copies between the revision we're
1551 1550 # rebasing and its first parent.
1552 1551 copies.graftcopies(wctx, ctx, ctx.p1())
1553 1552
1554 1553 if stats.unresolvedcount > 0:
1555 1554 if wctx.isinmemory():
1556 1555 raise error.InMemoryMergeConflictsError()
1557 1556 else:
1558 1557 raise error.ConflictResolutionRequired(b'rebase')
1559 1558
1560 1559
1561 1560 def adjustdest(repo, rev, destmap, state, skipped):
1562 1561 r"""adjust rebase destination given the current rebase state
1563 1562
1564 1563 rev is what is being rebased. Return a list of two revs, which are the
1565 1564 adjusted destinations for rev's p1 and p2, respectively. If a parent is
1566 1565 nullrev, return dest without adjustment for it.
1567 1566
1568 1567 For example, when doing rebasing B+E to F, C to G, rebase will first move B
1569 1568 to B1, and E's destination will be adjusted from F to B1.
1570 1569
1571 1570 B1 <- written during rebasing B
1572 1571 |
1573 1572 F <- original destination of B, E
1574 1573 |
1575 1574 | E <- rev, which is being rebased
1576 1575 | |
1577 1576 | D <- prev, one parent of rev being checked
1578 1577 | |
1579 1578 | x <- skipped, ex. no successor or successor in (::dest)
1580 1579 | |
1581 1580 | C <- rebased as C', different destination
1582 1581 | |
1583 1582 | B <- rebased as B1 C'
1584 1583 |/ |
1585 1584 A G <- destination of C, different
1586 1585
1587 1586 Another example about merge changeset, rebase -r C+G+H -d K, rebase will
1588 1587 first move C to C1, G to G1, and when it's checking H, the adjusted
1589 1588 destinations will be [C1, G1].
1590 1589
1591 1590 H C1 G1
1592 1591 /| | /
1593 1592 F G |/
1594 1593 K | | -> K
1595 1594 | C D |
1596 1595 | |/ |
1597 1596 | B | ...
1598 1597 |/ |/
1599 1598 A A
1600 1599
1601 1600 Besides, adjust dest according to existing rebase information. For example,
1602 1601
1603 1602 B C D B needs to be rebased on top of C, C needs to be rebased on top
1604 1603 \|/ of D. We will rebase C first.
1605 1604 A
1606 1605
1607 1606 C' After rebasing C, when considering B's destination, use C'
1608 1607 | instead of the original C.
1609 1608 B D
1610 1609 \ /
1611 1610 A
1612 1611 """
1613 1612 # pick already rebased revs with same dest from state as interesting source
1614 1613 dest = destmap[rev]
1615 1614 source = [
1616 1615 s
1617 1616 for s, d in state.items()
1618 1617 if d > 0 and destmap[s] == dest and s not in skipped
1619 1618 ]
1620 1619
1621 1620 result = []
1622 1621 for prev in repo.changelog.parentrevs(rev):
1623 1622 adjusted = dest
1624 1623 if prev != nullrev:
1625 1624 candidate = repo.revs(b'max(%ld and (::%d))', source, prev).first()
1626 1625 if candidate is not None:
1627 1626 adjusted = state[candidate]
1628 1627 if adjusted == dest and dest in state:
1629 1628 adjusted = state[dest]
1630 1629 if adjusted == revtodo:
1631 1630 # sortsource should produce an order that makes this impossible
1632 1631 raise error.ProgrammingError(
1633 1632 b'rev %d should be rebased already at this time' % dest
1634 1633 )
1635 1634 result.append(adjusted)
1636 1635 return result
1637 1636
1638 1637
1639 1638 def _checkobsrebase(repo, ui, rebaseobsrevs, rebaseobsskipped):
1640 1639 """
1641 1640 Abort if rebase will create divergence or rebase is noop because of markers
1642 1641
1643 1642 `rebaseobsrevs`: set of obsolete revision in source
1644 1643 `rebaseobsskipped`: set of revisions from source skipped because they have
1645 1644 successors in destination or no non-obsolete successor.
1646 1645 """
1647 1646 # Obsolete node with successors not in dest leads to divergence
1648 1647 divergenceok = obsolete.isenabled(repo, obsolete.allowdivergenceopt)
1649 1648 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
1650 1649
1651 1650 if divergencebasecandidates and not divergenceok:
1652 1651 divhashes = (bytes(repo[r]) for r in divergencebasecandidates)
1653 1652 msg = _(b"this rebase will cause divergences from: %s")
1654 1653 h = _(
1655 1654 b"to force the rebase please set "
1656 1655 b"experimental.evolution.allowdivergence=True"
1657 1656 )
1658 1657 raise error.StateError(msg % (b",".join(divhashes),), hint=h)
1659 1658
1660 1659
1661 1660 def successorrevs(unfi, rev):
1662 1661 """yield revision numbers for successors of rev"""
1663 1662 assert unfi.filtername is None
1664 1663 get_rev = unfi.changelog.index.get_rev
1665 1664 for s in obsutil.allsuccessors(unfi.obsstore, [unfi[rev].node()]):
1666 1665 r = get_rev(s)
1667 1666 if r is not None:
1668 1667 yield r
1669 1668
1670 1669
1671 1670 def defineparents(repo, rev, destmap, state, skipped, obsskipped):
1672 1671 """Return new parents and optionally a merge base for rev being rebased
1673 1672
1674 1673 The destination specified by "dest" cannot always be used directly because
1675 1674 previously rebase result could affect destination. For example,
1676 1675
1677 1676 D E rebase -r C+D+E -d B
1678 1677 |/ C will be rebased to C'
1679 1678 B C D's new destination will be C' instead of B
1680 1679 |/ E's new destination will be C' instead of B
1681 1680 A
1682 1681
1683 1682 The new parents of a merge is slightly more complicated. See the comment
1684 1683 block below.
1685 1684 """
1686 1685 # use unfiltered changelog since successorrevs may return filtered nodes
1687 1686 assert repo.filtername is None
1688 1687 cl = repo.changelog
1689 1688 isancestor = cl.isancestorrev
1690 1689
1691 1690 dest = destmap[rev]
1692 1691 oldps = repo.changelog.parentrevs(rev) # old parents
1693 1692 newps = [nullrev, nullrev] # new parents
1694 1693 dests = adjustdest(repo, rev, destmap, state, skipped)
1695 1694 bases = list(oldps) # merge base candidates, initially just old parents
1696 1695
1697 1696 if all(r == nullrev for r in oldps[1:]):
1698 1697 # For non-merge changeset, just move p to adjusted dest as requested.
1699 1698 newps[0] = dests[0]
1700 1699 else:
1701 1700 # For merge changeset, if we move p to dests[i] unconditionally, both
1702 1701 # parents may change and the end result looks like "the merge loses a
1703 1702 # parent", which is a surprise. This is a limit because "--dest" only
1704 1703 # accepts one dest per src.
1705 1704 #
1706 1705 # Therefore, only move p with reasonable conditions (in this order):
1707 1706 # 1. use dest, if dest is a descendent of (p or one of p's successors)
1708 1707 # 2. use p's rebased result, if p is rebased (state[p] > 0)
1709 1708 #
1710 1709 # Comparing with adjustdest, the logic here does some additional work:
1711 1710 # 1. decide which parents will not be moved towards dest
1712 1711 # 2. if the above decision is "no", should a parent still be moved
1713 1712 # because it was rebased?
1714 1713 #
1715 1714 # For example:
1716 1715 #
1717 1716 # C # "rebase -r C -d D" is an error since none of the parents
1718 1717 # /| # can be moved. "rebase -r B+C -d D" will move C's parent
1719 1718 # A B D # B (using rule "2."), since B will be rebased.
1720 1719 #
1721 1720 # The loop tries to be not rely on the fact that a Mercurial node has
1722 1721 # at most 2 parents.
1723 1722 for i, p in enumerate(oldps):
1724 1723 np = p # new parent
1725 1724 if any(isancestor(x, dests[i]) for x in successorrevs(repo, p)):
1726 1725 np = dests[i]
1727 1726 elif p in state and state[p] > 0:
1728 1727 np = state[p]
1729 1728
1730 1729 # If one parent becomes an ancestor of the other, drop the ancestor
1731 1730 for j, x in enumerate(newps[:i]):
1732 1731 if x == nullrev:
1733 1732 continue
1734 1733 if isancestor(np, x): # CASE-1
1735 1734 np = nullrev
1736 1735 elif isancestor(x, np): # CASE-2
1737 1736 newps[j] = np
1738 1737 np = nullrev
1739 1738 # New parents forming an ancestor relationship does not
1740 1739 # mean the old parents have a similar relationship. Do not
1741 1740 # set bases[x] to nullrev.
1742 1741 bases[j], bases[i] = bases[i], bases[j]
1743 1742
1744 1743 newps[i] = np
1745 1744
1746 1745 # "rebasenode" updates to new p1, and the old p1 will be used as merge
1747 1746 # base. If only p2 changes, merging using unchanged p1 as merge base is
1748 1747 # suboptimal. Therefore swap parents to make the merge sane.
1749 1748 if newps[1] != nullrev and oldps[0] == newps[0]:
1750 1749 assert len(newps) == 2 and len(oldps) == 2
1751 1750 newps.reverse()
1752 1751 bases.reverse()
1753 1752
1754 1753 # No parent change might be an error because we fail to make rev a
1755 1754 # descendent of requested dest. This can happen, for example:
1756 1755 #
1757 1756 # C # rebase -r C -d D
1758 1757 # /| # None of A and B will be changed to D and rebase fails.
1759 1758 # A B D
1760 1759 if set(newps) == set(oldps) and dest not in newps:
1761 1760 raise error.InputError(
1762 1761 _(
1763 1762 b'cannot rebase %d:%s without '
1764 1763 b'moving at least one of its parents'
1765 1764 )
1766 1765 % (rev, repo[rev])
1767 1766 )
1768 1767
1769 1768 # Source should not be ancestor of dest. The check here guarantees it's
1770 1769 # impossible. With multi-dest, the initial check does not cover complex
1771 1770 # cases since we don't have abstractions to dry-run rebase cheaply.
1772 1771 if any(p != nullrev and isancestor(rev, p) for p in newps):
1773 1772 raise error.InputError(_(b'source is ancestor of destination'))
1774 1773
1775 1774 # Check if the merge will contain unwanted changes. That may happen if
1776 1775 # there are multiple special (non-changelog ancestor) merge bases, which
1777 1776 # cannot be handled well by the 3-way merge algorithm. For example:
1778 1777 #
1779 1778 # F
1780 1779 # /|
1781 1780 # D E # "rebase -r D+E+F -d Z", when rebasing F, if "D" was chosen
1782 1781 # | | # as merge base, the difference between D and F will include
1783 1782 # B C # C, so the rebased F will contain C surprisingly. If "E" was
1784 1783 # |/ # chosen, the rebased F will contain B.
1785 1784 # A Z
1786 1785 #
1787 1786 # But our merge base candidates (D and E in above case) could still be
1788 1787 # better than the default (ancestor(F, Z) == null). Therefore still
1789 1788 # pick one (so choose p1 above).
1790 1789 if sum(1 for b in set(bases) if b != nullrev and b not in newps) > 1:
1791 1790 unwanted = [None, None] # unwanted[i]: unwanted revs if choose bases[i]
1792 1791 for i, base in enumerate(bases):
1793 1792 if base == nullrev or base in newps:
1794 1793 continue
1795 1794 # Revisions in the side (not chosen as merge base) branch that
1796 1795 # might contain "surprising" contents
1797 1796 other_bases = set(bases) - {base}
1798 1797 siderevs = list(
1799 1798 repo.revs(b'(%ld %% (%d+%d))', other_bases, base, dest)
1800 1799 )
1801 1800
1802 1801 # If those revisions are covered by rebaseset, the result is good.
1803 1802 # A merge in rebaseset would be considered to cover its ancestors.
1804 1803 if siderevs:
1805 1804 rebaseset = [
1806 1805 r for r, d in state.items() if d > 0 and r not in obsskipped
1807 1806 ]
1808 1807 merges = [
1809 1808 r for r in rebaseset if cl.parentrevs(r)[1] != nullrev
1810 1809 ]
1811 1810 unwanted[i] = list(
1812 1811 repo.revs(
1813 1812 b'%ld - (::%ld) - %ld', siderevs, merges, rebaseset
1814 1813 )
1815 1814 )
1816 1815
1817 1816 if any(revs is not None for revs in unwanted):
1818 1817 # Choose a merge base that has a minimal number of unwanted revs.
1819 1818 l, i = min(
1820 1819 (len(revs), i)
1821 1820 for i, revs in enumerate(unwanted)
1822 1821 if revs is not None
1823 1822 )
1824 1823
1825 1824 # The merge will include unwanted revisions. Abort now. Revisit this if
1826 1825 # we have a more advanced merge algorithm that handles multiple bases.
1827 1826 if l > 0:
1828 1827 unwanteddesc = _(b' or ').join(
1829 1828 (
1830 1829 b', '.join(b'%d:%s' % (r, repo[r]) for r in revs)
1831 1830 for revs in unwanted
1832 1831 if revs is not None
1833 1832 )
1834 1833 )
1835 1834 raise error.InputError(
1836 1835 _(b'rebasing %d:%s will include unwanted changes from %s')
1837 1836 % (rev, repo[rev], unwanteddesc)
1838 1837 )
1839 1838
1840 1839 # newps[0] should match merge base if possible. Currently, if newps[i]
1841 1840 # is nullrev, the only case is newps[i] and newps[j] (j < i), one is
1842 1841 # the other's ancestor. In that case, it's fine to not swap newps here.
1843 1842 # (see CASE-1 and CASE-2 above)
1844 1843 if i != 0:
1845 1844 if newps[i] != nullrev:
1846 1845 newps[0], newps[i] = newps[i], newps[0]
1847 1846 bases[0], bases[i] = bases[i], bases[0]
1848 1847
1849 1848 # "rebasenode" updates to new p1, use the corresponding merge base.
1850 1849 base = bases[0]
1851 1850
1852 1851 repo.ui.debug(b" future parents are %d and %d\n" % tuple(newps))
1853 1852
1854 1853 return newps[0], newps[1], base
1855 1854
1856 1855
1857 1856 def isagitpatch(repo, patchname):
1858 1857 """Return true if the given patch is in git format"""
1859 1858 mqpatch = os.path.join(repo.mq.path, patchname)
1860 for line in patch.linereader(open(mqpatch, b'rb')):
1859 for line in patch.linereader(open(mqpatch, 'rb')):
1861 1860 if line.startswith(b'diff --git'):
1862 1861 return True
1863 1862 return False
1864 1863
1865 1864
1866 1865 def updatemq(repo, state, skipped, **opts):
1867 1866 """Update rebased mq patches - finalize and then import them"""
1868 1867 mqrebase = {}
1869 1868 mq = repo.mq
1870 1869 original_series = mq.fullseries[:]
1871 1870 skippedpatches = set()
1872 1871
1873 1872 for p in mq.applied:
1874 1873 rev = repo[p.node].rev()
1875 1874 if rev in state:
1876 1875 repo.ui.debug(
1877 1876 b'revision %d is an mq patch (%s), finalize it.\n'
1878 1877 % (rev, p.name)
1879 1878 )
1880 1879 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
1881 1880 else:
1882 1881 # Applied but not rebased, not sure this should happen
1883 1882 skippedpatches.add(p.name)
1884 1883
1885 1884 if mqrebase:
1886 1885 mq.finish(repo, mqrebase.keys())
1887 1886
1888 1887 # We must start import from the newest revision
1889 1888 for rev in sorted(mqrebase, reverse=True):
1890 1889 if rev not in skipped:
1891 1890 name, isgit = mqrebase[rev]
1892 1891 repo.ui.note(
1893 1892 _(b'updating mq patch %s to %d:%s\n')
1894 1893 % (name, state[rev], repo[state[rev]])
1895 1894 )
1896 1895 mq.qimport(
1897 1896 repo,
1898 1897 (),
1899 1898 patchname=name,
1900 1899 git=isgit,
1901 1900 rev=[b"%d" % state[rev]],
1902 1901 )
1903 1902 else:
1904 1903 # Rebased and skipped
1905 1904 skippedpatches.add(mqrebase[rev][0])
1906 1905
1907 1906 # Patches were either applied and rebased and imported in
1908 1907 # order, applied and removed or unapplied. Discard the removed
1909 1908 # ones while preserving the original series order and guards.
1910 1909 newseries = [
1911 1910 s
1912 1911 for s in original_series
1913 1912 if mq.guard_re.split(s, 1)[0] not in skippedpatches
1914 1913 ]
1915 1914 mq.fullseries[:] = newseries
1916 1915 mq.seriesdirty = True
1917 1916 mq.savedirty()
1918 1917
1919 1918
1920 1919 def storecollapsemsg(repo, collapsemsg):
1921 1920 """Store the collapse message to allow recovery"""
1922 1921 collapsemsg = collapsemsg or b''
1923 1922 f = repo.vfs(b"last-message.txt", b"w")
1924 1923 f.write(b"%s\n" % collapsemsg)
1925 1924 f.close()
1926 1925
1927 1926
1928 1927 def clearcollapsemsg(repo):
1929 1928 """Remove collapse message file"""
1930 1929 repo.vfs.unlinkpath(b"last-message.txt", ignoremissing=True)
1931 1930
1932 1931
1933 1932 def restorecollapsemsg(repo, isabort):
1934 1933 """Restore previously stored collapse message"""
1935 1934 try:
1936 1935 f = repo.vfs(b"last-message.txt")
1937 1936 collapsemsg = f.readline().strip()
1938 1937 f.close()
1939 1938 except FileNotFoundError:
1940 1939 if isabort:
1941 1940 # Oh well, just abort like normal
1942 1941 collapsemsg = b''
1943 1942 else:
1944 1943 raise error.Abort(_(b'missing .hg/last-message.txt for rebase'))
1945 1944 return collapsemsg
1946 1945
1947 1946
1948 1947 def clearstatus(repo):
1949 1948 """Remove the status files"""
1950 1949 # Make sure the active transaction won't write the state file
1951 1950 tr = repo.currenttransaction()
1952 1951 if tr:
1953 1952 tr.removefilegenerator(b'rebasestate')
1954 1953 repo.vfs.unlinkpath(b"rebasestate", ignoremissing=True)
1955 1954
1956 1955
1957 1956 def sortsource(destmap):
1958 1957 """yield source revisions in an order that we only rebase things once
1959 1958
1960 1959 If source and destination overlaps, we should filter out revisions
1961 1960 depending on other revisions which hasn't been rebased yet.
1962 1961
1963 1962 Yield a sorted list of revisions each time.
1964 1963
1965 1964 For example, when rebasing A to B, B to C. This function yields [B], then
1966 1965 [A], indicating B needs to be rebased first.
1967 1966
1968 1967 Raise if there is a cycle so the rebase is impossible.
1969 1968 """
1970 1969 srcset = set(destmap)
1971 1970 while srcset:
1972 1971 srclist = sorted(srcset)
1973 1972 result = []
1974 1973 for r in srclist:
1975 1974 if destmap[r] not in srcset:
1976 1975 result.append(r)
1977 1976 if not result:
1978 1977 raise error.InputError(_(b'source and destination form a cycle'))
1979 1978 srcset -= set(result)
1980 1979 yield result
1981 1980
1982 1981
1983 1982 def buildstate(repo, destmap, collapse):
1984 1983 """Define which revisions are going to be rebased and where
1985 1984
1986 1985 repo: repo
1987 1986 destmap: {srcrev: destrev}
1988 1987 """
1989 1988 rebaseset = destmap.keys()
1990 1989 originalwd = repo[b'.'].rev()
1991 1990
1992 1991 # This check isn't strictly necessary, since mq detects commits over an
1993 1992 # applied patch. But it prevents messing up the working directory when
1994 1993 # a partially completed rebase is blocked by mq.
1995 1994 if b'qtip' in repo.tags():
1996 1995 mqapplied = {repo[s.node].rev() for s in repo.mq.applied}
1997 1996 if set(destmap.values()) & mqapplied:
1998 1997 raise error.StateError(_(b'cannot rebase onto an applied mq patch'))
1999 1998
2000 1999 # Get "cycle" error early by exhausting the generator.
2001 2000 sortedsrc = list(sortsource(destmap)) # a list of sorted revs
2002 2001 if not sortedsrc:
2003 2002 raise error.InputError(_(b'no matching revisions'))
2004 2003
2005 2004 # Only check the first batch of revisions to rebase not depending on other
2006 2005 # rebaseset. This means "source is ancestor of destination" for the second
2007 2006 # (and following) batches of revisions are not checked here. We rely on
2008 2007 # "defineparents" to do that check.
2009 2008 roots = list(repo.set(b'roots(%ld)', sortedsrc[0]))
2010 2009 if not roots:
2011 2010 raise error.InputError(_(b'no matching revisions'))
2012 2011
2013 2012 def revof(r):
2014 2013 return r.rev()
2015 2014
2016 2015 roots = sorted(roots, key=revof)
2017 2016 state = dict.fromkeys(rebaseset, revtodo)
2018 2017 emptyrebase = len(sortedsrc) == 1
2019 2018 for root in roots:
2020 2019 dest = repo[destmap[root.rev()]]
2021 2020 commonbase = root.ancestor(dest)
2022 2021 if commonbase == root:
2023 2022 raise error.InputError(_(b'source is ancestor of destination'))
2024 2023 if commonbase == dest:
2025 2024 wctx = repo[None]
2026 2025 if dest == wctx.p1():
2027 2026 # when rebasing to '.', it will use the current wd branch name
2028 2027 samebranch = root.branch() == wctx.branch()
2029 2028 else:
2030 2029 samebranch = root.branch() == dest.branch()
2031 2030 if not collapse and samebranch and dest in root.parents():
2032 2031 # mark the revision as done by setting its new revision
2033 2032 # equal to its old (current) revisions
2034 2033 state[root.rev()] = root.rev()
2035 2034 repo.ui.debug(b'source is a child of destination\n')
2036 2035 continue
2037 2036
2038 2037 emptyrebase = False
2039 2038 repo.ui.debug(b'rebase onto %s starting from %s\n' % (dest, root))
2040 2039 if emptyrebase:
2041 2040 return None
2042 2041 for rev in sorted(state):
2043 2042 parents = [p for p in repo.changelog.parentrevs(rev) if p != nullrev]
2044 2043 # if all parents of this revision are done, then so is this revision
2045 2044 if parents and all((state.get(p) == p for p in parents)):
2046 2045 state[rev] = rev
2047 2046 return originalwd, destmap, state
2048 2047
2049 2048
2050 2049 def clearrebased(
2051 2050 ui,
2052 2051 repo,
2053 2052 destmap,
2054 2053 state,
2055 2054 skipped,
2056 2055 collapsedas=None,
2057 2056 keepf=False,
2058 2057 fm=None,
2059 2058 backup=True,
2060 2059 ):
2061 2060 """dispose of rebased revision at the end of the rebase
2062 2061
2063 2062 If `collapsedas` is not None, the rebase was a collapse whose result if the
2064 2063 `collapsedas` node.
2065 2064
2066 2065 If `keepf` is not True, the rebase has --keep set and no nodes should be
2067 2066 removed (but bookmarks still need to be moved).
2068 2067
2069 2068 If `backup` is False, no backup will be stored when stripping rebased
2070 2069 revisions.
2071 2070 """
2072 2071 tonode = repo.changelog.node
2073 2072 replacements = {}
2074 2073 moves = {}
2075 2074 stripcleanup = not obsolete.isenabled(repo, obsolete.createmarkersopt)
2076 2075
2077 2076 collapsednodes = []
2078 2077 for rev, newrev in sorted(state.items()):
2079 2078 if newrev >= 0 and newrev != rev:
2080 2079 oldnode = tonode(rev)
2081 2080 newnode = collapsedas or tonode(newrev)
2082 2081 moves[oldnode] = newnode
2083 2082 succs = None
2084 2083 if rev in skipped:
2085 2084 if stripcleanup or not repo[rev].obsolete():
2086 2085 succs = ()
2087 2086 elif collapsedas:
2088 2087 collapsednodes.append(oldnode)
2089 2088 else:
2090 2089 succs = (newnode,)
2091 2090 if succs is not None:
2092 2091 replacements[(oldnode,)] = succs
2093 2092 if collapsednodes:
2094 2093 replacements[tuple(collapsednodes)] = (collapsedas,)
2095 2094 if fm:
2096 2095 hf = fm.hexfunc
2097 2096 fl = fm.formatlist
2098 2097 fd = fm.formatdict
2099 2098 changes = {}
2100 2099 for oldns, newn in replacements.items():
2101 2100 for oldn in oldns:
2102 2101 changes[hf(oldn)] = fl([hf(n) for n in newn], name=b'node')
2103 2102 nodechanges = fd(changes, key=b"oldnode", value=b"newnodes")
2104 2103 fm.data(nodechanges=nodechanges)
2105 2104 if keepf:
2106 2105 replacements = {}
2107 2106 scmutil.cleanupnodes(repo, replacements, b'rebase', moves, backup=backup)
2108 2107
2109 2108
2110 2109 def pullrebase(orig, ui, repo, *args, **opts):
2111 2110 """Call rebase after pull if the latter has been invoked with --rebase"""
2112 2111 if opts.get('rebase'):
2113 2112 if ui.configbool(b'commands', b'rebase.requiredest'):
2114 2113 msg = _(b'rebase destination required by configuration')
2115 2114 hint = _(b'use hg pull followed by hg rebase -d DEST')
2116 2115 raise error.InputError(msg, hint=hint)
2117 2116
2118 2117 with repo.wlock(), repo.lock():
2119 2118 if opts.get('update'):
2120 2119 del opts['update']
2121 2120 ui.debug(
2122 2121 b'--update and --rebase are not compatible, ignoring '
2123 2122 b'the update flag\n'
2124 2123 )
2125 2124
2126 2125 cmdutil.checkunfinished(repo, skipmerge=True)
2127 2126 cmdutil.bailifchanged(
2128 2127 repo,
2129 2128 hint=_(
2130 2129 b'cannot pull with rebase: '
2131 2130 b'please commit or shelve your changes first'
2132 2131 ),
2133 2132 )
2134 2133
2135 2134 revsprepull = len(repo)
2136 2135 origpostincoming = cmdutil.postincoming
2137 2136
2138 2137 def _dummy(*args, **kwargs):
2139 2138 pass
2140 2139
2141 2140 cmdutil.postincoming = _dummy
2142 2141 try:
2143 2142 ret = orig(ui, repo, *args, **opts)
2144 2143 finally:
2145 2144 cmdutil.postincoming = origpostincoming
2146 2145 revspostpull = len(repo)
2147 2146 if revspostpull > revsprepull:
2148 2147 # --rev option from pull conflict with rebase own --rev
2149 2148 # dropping it
2150 2149 if 'rev' in opts:
2151 2150 del opts['rev']
2152 2151 # positional argument from pull conflicts with rebase's own
2153 2152 # --source.
2154 2153 if 'source' in opts:
2155 2154 del opts['source']
2156 2155 # revsprepull is the len of the repo, not revnum of tip.
2157 2156 destspace = list(repo.changelog.revs(start=revsprepull))
2158 2157 opts['_destspace'] = destspace
2159 2158 try:
2160 2159 rebase(ui, repo, **opts)
2161 2160 except error.NoMergeDestAbort:
2162 2161 # we can maybe update instead
2163 2162 rev, _a, _b = destutil.destupdate(repo)
2164 2163 if rev == repo[b'.'].rev():
2165 2164 ui.status(_(b'nothing to rebase\n'))
2166 2165 else:
2167 2166 ui.status(_(b'nothing to rebase - updating instead\n'))
2168 2167 # not passing argument to get the bare update behavior
2169 2168 # with warning and trumpets
2170 2169 commands.update(ui, repo)
2171 2170 else:
2172 2171 if opts.get('tool'):
2173 2172 raise error.InputError(_(b'--tool can only be used with --rebase'))
2174 2173 ret = orig(ui, repo, *args, **opts)
2175 2174
2176 2175 return ret
2177 2176
2178 2177
2179 2178 def _compute_obsolete_sets(repo, rebaseobsrevs, destmap):
2180 2179 """Figure out what to do about about obsolete revisions
2181 2180
2182 2181 `obsolete_with_successor_in_destination` is a mapping mapping obsolete => successor for all
2183 2182 obsolete nodes to be rebased given in `rebaseobsrevs`.
2184 2183
2185 2184 `obsolete_with_successor_in_rebase_set` is a set with obsolete revisions,
2186 2185 without a successor in destination, that would cause divergence.
2187 2186 """
2188 2187 obsolete_with_successor_in_destination = {}
2189 2188 obsolete_with_successor_in_rebase_set = set()
2190 2189
2191 2190 cl = repo.changelog
2192 2191 get_rev = cl.index.get_rev
2193 2192 extinctrevs = set(repo.revs(b'extinct()'))
2194 2193 for srcrev in rebaseobsrevs:
2195 2194 srcnode = cl.node(srcrev)
2196 2195 # XXX: more advanced APIs are required to handle split correctly
2197 2196 successors = set(obsutil.allsuccessors(repo.obsstore, [srcnode]))
2198 2197 # obsutil.allsuccessors includes node itself
2199 2198 successors.remove(srcnode)
2200 2199 succrevs = {get_rev(s) for s in successors}
2201 2200 succrevs.discard(None)
2202 2201 if not successors or succrevs.issubset(extinctrevs):
2203 2202 # no successor, or all successors are extinct
2204 2203 obsolete_with_successor_in_destination[srcrev] = None
2205 2204 else:
2206 2205 dstrev = destmap[srcrev]
2207 2206 for succrev in succrevs:
2208 2207 if cl.isancestorrev(succrev, dstrev):
2209 2208 obsolete_with_successor_in_destination[srcrev] = succrev
2210 2209 break
2211 2210 else:
2212 2211 # If 'srcrev' has a successor in rebase set but none in
2213 2212 # destination (which would be catched above), we shall skip it
2214 2213 # and its descendants to avoid divergence.
2215 2214 if srcrev in extinctrevs or any(s in destmap for s in succrevs):
2216 2215 obsolete_with_successor_in_rebase_set.add(srcrev)
2217 2216
2218 2217 return (
2219 2218 obsolete_with_successor_in_destination,
2220 2219 obsolete_with_successor_in_rebase_set,
2221 2220 )
2222 2221
2223 2222
2224 2223 def abortrebase(ui, repo):
2225 2224 with repo.wlock(), repo.lock():
2226 2225 rbsrt = rebaseruntime(repo, ui)
2227 2226 rbsrt._prepareabortorcontinue(isabort=True)
2228 2227
2229 2228
2230 2229 def continuerebase(ui, repo):
2231 2230 with repo.wlock(), repo.lock():
2232 2231 rbsrt = rebaseruntime(repo, ui)
2233 2232 ms = mergestatemod.mergestate.read(repo)
2234 2233 mergeutil.checkunresolved(ms)
2235 2234 retcode = rbsrt._prepareabortorcontinue(isabort=False)
2236 2235 if retcode is not None:
2237 2236 return retcode
2238 2237 rbsrt._performrebase(None)
2239 2238 rbsrt._finishrebase()
2240 2239
2241 2240
2242 2241 def summaryhook(ui, repo):
2243 2242 if not repo.vfs.exists(b'rebasestate'):
2244 2243 return
2245 2244 try:
2246 2245 rbsrt = rebaseruntime(repo, ui, {})
2247 2246 rbsrt.restorestatus()
2248 2247 state = rbsrt.state
2249 2248 except error.RepoLookupError:
2250 2249 # i18n: column positioning for "hg summary"
2251 2250 msg = _(b'rebase: (use "hg rebase --abort" to clear broken state)\n')
2252 2251 ui.write(msg)
2253 2252 return
2254 2253 numrebased = len([i for i in state.values() if i >= 0])
2255 2254 # i18n: column positioning for "hg summary"
2256 2255 ui.write(
2257 2256 _(b'rebase: %s, %s (rebase --continue)\n')
2258 2257 % (
2259 2258 ui.label(_(b'%d rebased'), b'rebase.rebased') % numrebased,
2260 2259 ui.label(_(b'%d remaining'), b'rebase.remaining')
2261 2260 % (len(state) - numrebased),
2262 2261 )
2263 2262 )
2264 2263
2265 2264
2266 2265 def uisetup(ui):
2267 2266 # Replace pull with a decorator to provide --rebase option
2268 2267 entry = extensions.wrapcommand(commands.table, b'pull', pullrebase)
2269 2268 entry[1].append(
2270 2269 (b'', b'rebase', None, _(b"rebase working directory to branch head"))
2271 2270 )
2272 2271 entry[1].append((b't', b'tool', b'', _(b"specify merge tool for rebase")))
2273 2272 cmdutil.summaryhooks.add(b'rebase', summaryhook)
2274 2273 statemod.addunfinished(
2275 2274 b'rebase',
2276 2275 fname=b'rebasestate',
2277 2276 stopflag=True,
2278 2277 continueflag=True,
2279 2278 abortfunc=abortrebase,
2280 2279 continuefunc=continuerebase,
2281 2280 )
General Comments 0
You need to be logged in to leave comments. Login now