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