##// END OF EJS Templates
rebase: fix bug that caused transitive copy records to disappear (issue4192)...
Augie Fackler -
r21826:2ba6c9b4 stable
parent child Browse files
Show More
@@ -1,962 +1,971 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 http://mercurial.selenic.com/wiki/RebaseExtension
15 15 '''
16 16
17 17 from mercurial import hg, util, repair, merge, cmdutil, commands, bookmarks
18 18 from mercurial import extensions, patch, scmutil, phases, obsolete, error
19 19 from mercurial.commands import templateopts
20 20 from mercurial.node import nullrev
21 21 from mercurial.lock import release
22 22 from mercurial.i18n import _
23 23 import os, errno
24 24
25 25 nullmerge = -2
26 26 revignored = -3
27 27
28 28 cmdtable = {}
29 29 command = cmdutil.command(cmdtable)
30 30 testedwith = 'internal'
31 31
32 32 def _savegraft(ctx, extra):
33 33 s = ctx.extra().get('source', None)
34 34 if s is not None:
35 35 extra['source'] = s
36 36
37 37 def _savebranch(ctx, extra):
38 38 extra['branch'] = ctx.branch()
39 39
40 40 def _makeextrafn(copiers):
41 41 """make an extrafn out of the given copy-functions.
42 42
43 43 A copy function takes a context and an extra dict, and mutates the
44 44 extra dict as needed based on the given context.
45 45 """
46 46 def extrafn(ctx, extra):
47 47 for c in copiers:
48 48 c(ctx, extra)
49 49 return extrafn
50 50
51 51 @command('rebase',
52 52 [('s', 'source', '',
53 53 _('rebase from the specified changeset'), _('REV')),
54 54 ('b', 'base', '',
55 55 _('rebase from the base of the specified changeset '
56 56 '(up to greatest common ancestor of base and dest)'),
57 57 _('REV')),
58 58 ('r', 'rev', [],
59 59 _('rebase these revisions'),
60 60 _('REV')),
61 61 ('d', 'dest', '',
62 62 _('rebase onto the specified changeset'), _('REV')),
63 63 ('', 'collapse', False, _('collapse the rebased changesets')),
64 64 ('m', 'message', '',
65 65 _('use text as collapse commit message'), _('TEXT')),
66 66 ('e', 'edit', False, _('invoke editor on commit messages')),
67 67 ('l', 'logfile', '',
68 68 _('read collapse commit message from file'), _('FILE')),
69 69 ('', 'keep', False, _('keep original changesets')),
70 70 ('', 'keepbranches', False, _('keep original branch names')),
71 71 ('D', 'detach', False, _('(DEPRECATED)')),
72 72 ('t', 'tool', '', _('specify merge tool')),
73 73 ('c', 'continue', False, _('continue an interrupted rebase')),
74 74 ('a', 'abort', False, _('abort an interrupted rebase'))] +
75 75 templateopts,
76 76 _('[-s REV | -b REV] [-d REV] [OPTION]'))
77 77 def rebase(ui, repo, **opts):
78 78 """move changeset (and descendants) to a different branch
79 79
80 80 Rebase uses repeated merging to graft changesets from one part of
81 81 history (the source) onto another (the destination). This can be
82 82 useful for linearizing *local* changes relative to a master
83 83 development tree.
84 84
85 85 You should not rebase changesets that have already been shared
86 86 with others. Doing so will force everybody else to perform the
87 87 same rebase or they will end up with duplicated changesets after
88 88 pulling in your rebased changesets.
89 89
90 90 In its default configuration, Mercurial will prevent you from
91 91 rebasing published changes. See :hg:`help phases` for details.
92 92
93 93 If you don't specify a destination changeset (``-d/--dest``),
94 94 rebase uses the current branch tip as the destination. (The
95 95 destination changeset is not modified by rebasing, but new
96 96 changesets are added as its descendants.)
97 97
98 98 You can specify which changesets to rebase in two ways: as a
99 99 "source" changeset or as a "base" changeset. Both are shorthand
100 100 for a topologically related set of changesets (the "source
101 101 branch"). If you specify source (``-s/--source``), rebase will
102 102 rebase that changeset and all of its descendants onto dest. If you
103 103 specify base (``-b/--base``), rebase will select ancestors of base
104 104 back to but not including the common ancestor with dest. Thus,
105 105 ``-b`` is less precise but more convenient than ``-s``: you can
106 106 specify any changeset in the source branch, and rebase will select
107 107 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
108 108 uses the parent of the working directory as the base.
109 109
110 110 For advanced usage, a third way is available through the ``--rev``
111 111 option. It allows you to specify an arbitrary set of changesets to
112 112 rebase. Descendants of revs you specify with this option are not
113 113 automatically included in the rebase.
114 114
115 115 By default, rebase recreates the changesets in the source branch
116 116 as descendants of dest and then destroys the originals. Use
117 117 ``--keep`` to preserve the original source changesets. Some
118 118 changesets in the source branch (e.g. merges from the destination
119 119 branch) may be dropped if they no longer contribute any change.
120 120
121 121 One result of the rules for selecting the destination changeset
122 122 and source branch is that, unlike ``merge``, rebase will do
123 123 nothing if you are at the branch tip of a named branch
124 124 with two heads. You need to explicitly specify source and/or
125 125 destination (or ``update`` to the other head, if it's the head of
126 126 the intended source branch).
127 127
128 128 If a rebase is interrupted to manually resolve a merge, it can be
129 129 continued with --continue/-c or aborted with --abort/-a.
130 130
131 131 Returns 0 on success, 1 if nothing to rebase or there are
132 132 unresolved conflicts.
133 133 """
134 134 originalwd = target = None
135 135 activebookmark = None
136 136 external = nullrev
137 137 state = {}
138 138 skipped = set()
139 139 targetancestors = set()
140 140
141 141 editor = None
142 142 if opts.get('edit'):
143 143 editor = cmdutil.commitforceeditor
144 144
145 145 lock = wlock = None
146 146 try:
147 147 wlock = repo.wlock()
148 148 lock = repo.lock()
149 149
150 150 # Validate input and define rebasing points
151 151 destf = opts.get('dest', None)
152 152 srcf = opts.get('source', None)
153 153 basef = opts.get('base', None)
154 154 revf = opts.get('rev', [])
155 155 contf = opts.get('continue')
156 156 abortf = opts.get('abort')
157 157 collapsef = opts.get('collapse', False)
158 158 collapsemsg = cmdutil.logmessage(ui, opts)
159 159 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
160 160 extrafns = [_savegraft]
161 161 if e:
162 162 extrafns = [e]
163 163 keepf = opts.get('keep', False)
164 164 keepbranchesf = opts.get('keepbranches', False)
165 165 # keepopen is not meant for use on the command line, but by
166 166 # other extensions
167 167 keepopen = opts.get('keepopen', False)
168 168
169 169 if collapsemsg and not collapsef:
170 170 raise util.Abort(
171 171 _('message can only be specified with collapse'))
172 172
173 173 if contf or abortf:
174 174 if contf and abortf:
175 175 raise util.Abort(_('cannot use both abort and continue'))
176 176 if collapsef:
177 177 raise util.Abort(
178 178 _('cannot use collapse with continue or abort'))
179 179 if srcf or basef or destf:
180 180 raise util.Abort(
181 181 _('abort and continue do not allow specifying revisions'))
182 182 if opts.get('tool', False):
183 183 ui.warn(_('tool option will be ignored\n'))
184 184
185 185 try:
186 186 (originalwd, target, state, skipped, collapsef, keepf,
187 187 keepbranchesf, external, activebookmark) = restorestatus(repo)
188 188 except error.RepoLookupError:
189 189 if abortf:
190 190 clearstatus(repo)
191 191 repo.ui.warn(_('rebase aborted (no revision is removed,'
192 192 ' only broken state is cleared)\n'))
193 193 return 0
194 194 else:
195 195 msg = _('cannot continue inconsistent rebase')
196 196 hint = _('use "hg rebase --abort" to clear broken state')
197 197 raise util.Abort(msg, hint=hint)
198 198 if abortf:
199 199 return abort(repo, originalwd, target, state)
200 200 else:
201 201 if srcf and basef:
202 202 raise util.Abort(_('cannot specify both a '
203 203 'source and a base'))
204 204 if revf and basef:
205 205 raise util.Abort(_('cannot specify both a '
206 206 'revision and a base'))
207 207 if revf and srcf:
208 208 raise util.Abort(_('cannot specify both a '
209 209 'revision and a source'))
210 210
211 211 cmdutil.checkunfinished(repo)
212 212 cmdutil.bailifchanged(repo)
213 213
214 214 if not destf:
215 215 # Destination defaults to the latest revision in the
216 216 # current branch
217 217 branch = repo[None].branch()
218 218 dest = repo[branch]
219 219 else:
220 220 dest = scmutil.revsingle(repo, destf)
221 221
222 222 if revf:
223 223 rebaseset = scmutil.revrange(repo, revf)
224 224 if not rebaseset:
225 225 ui.status(_('empty "rev" revision set - '
226 226 'nothing to rebase\n'))
227 227 return 1
228 228 elif srcf:
229 229 src = scmutil.revrange(repo, [srcf])
230 230 if not src:
231 231 ui.status(_('empty "source" revision set - '
232 232 'nothing to rebase\n'))
233 233 return 1
234 234 rebaseset = repo.revs('(%ld)::', src)
235 235 assert rebaseset
236 236 else:
237 237 base = scmutil.revrange(repo, [basef or '.'])
238 238 if not base:
239 239 ui.status(_('empty "base" revision set - '
240 240 "can't compute rebase set\n"))
241 241 return 1
242 242 rebaseset = repo.revs(
243 243 '(children(ancestor(%ld, %d)) and ::(%ld))::',
244 244 base, dest, base)
245 245 if not rebaseset:
246 246 if base == [dest.rev()]:
247 247 if basef:
248 248 ui.status(_('nothing to rebase - %s is both "base"'
249 249 ' and destination\n') % dest)
250 250 else:
251 251 ui.status(_('nothing to rebase - working directory '
252 252 'parent is also destination\n'))
253 253 elif not repo.revs('%ld - ::%d', base, dest):
254 254 if basef:
255 255 ui.status(_('nothing to rebase - "base" %s is '
256 256 'already an ancestor of destination '
257 257 '%s\n') %
258 258 ('+'.join(str(repo[r]) for r in base),
259 259 dest))
260 260 else:
261 261 ui.status(_('nothing to rebase - working '
262 262 'directory parent is already an '
263 263 'ancestor of destination %s\n') % dest)
264 264 else: # can it happen?
265 265 ui.status(_('nothing to rebase from %s to %s\n') %
266 266 ('+'.join(str(repo[r]) for r in base), dest))
267 267 return 1
268 268
269 269 if (not (keepf or obsolete._enabled)
270 270 and repo.revs('first(children(%ld) - %ld)',
271 271 rebaseset, rebaseset)):
272 272 raise util.Abort(
273 273 _("can't remove original changesets with"
274 274 " unrebased descendants"),
275 275 hint=_('use --keep to keep original changesets'))
276 276
277 277 result = buildstate(repo, dest, rebaseset, collapsef)
278 278 if not result:
279 279 # Empty state built, nothing to rebase
280 280 ui.status(_('nothing to rebase\n'))
281 281 return 1
282 282
283 283 root = min(rebaseset)
284 284 if not keepf and not repo[root].mutable():
285 285 raise util.Abort(_("can't rebase immutable changeset %s")
286 286 % repo[root],
287 287 hint=_('see hg help phases for details'))
288 288
289 289 originalwd, target, state = result
290 290 if collapsef:
291 291 targetancestors = repo.changelog.ancestors([target],
292 292 inclusive=True)
293 293 external = externalparent(repo, state, targetancestors)
294 294
295 295 if dest.closesbranch() and not keepbranchesf:
296 296 ui.status(_('reopening closed branch head %s\n') % dest)
297 297
298 298 if keepbranchesf:
299 299 # insert _savebranch at the start of extrafns so if
300 300 # there's a user-provided extrafn it can clobber branch if
301 301 # desired
302 302 extrafns.insert(0, _savebranch)
303 303 if collapsef:
304 304 branches = set()
305 305 for rev in state:
306 306 branches.add(repo[rev].branch())
307 307 if len(branches) > 1:
308 308 raise util.Abort(_('cannot collapse multiple named '
309 309 'branches'))
310 310
311 311 # Rebase
312 312 if not targetancestors:
313 313 targetancestors = repo.changelog.ancestors([target], inclusive=True)
314 314
315 315 # Keep track of the current bookmarks in order to reset them later
316 316 currentbookmarks = repo._bookmarks.copy()
317 317 activebookmark = activebookmark or repo._bookmarkcurrent
318 318 if activebookmark:
319 319 bookmarks.unsetcurrent(repo)
320 320
321 321 extrafn = _makeextrafn(extrafns)
322 322
323 323 sortedstate = sorted(state)
324 324 total = len(sortedstate)
325 325 pos = 0
326 326 for rev in sortedstate:
327 327 pos += 1
328 328 if state[rev] == -1:
329 329 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
330 330 _('changesets'), total)
331 331 p1, p2 = defineparents(repo, rev, target, state,
332 332 targetancestors)
333 333 storestatus(repo, originalwd, target, state, collapsef, keepf,
334 334 keepbranchesf, external, activebookmark)
335 335 if len(repo.parents()) == 2:
336 336 repo.ui.debug('resuming interrupted rebase\n')
337 337 else:
338 338 try:
339 339 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
340 340 'rebase')
341 341 stats = rebasenode(repo, rev, p1, state, collapsef)
342 342 if stats and stats[3] > 0:
343 343 raise error.InterventionRequired(
344 344 _('unresolved conflicts (see hg '
345 345 'resolve, then hg rebase --continue)'))
346 346 finally:
347 347 ui.setconfig('ui', 'forcemerge', '', 'rebase')
348 if collapsef:
348 349 cmdutil.duplicatecopies(repo, rev, target)
350 else:
351 # If we're not using --collapse, we need to
352 # duplicate copies between the revision we're
353 # rebasing and its first parent, but *not*
354 # duplicate any copies that have already been
355 # performed in the destination.
356 p1rev = repo[rev].p1().rev()
357 cmdutil.duplicatecopies(repo, rev, p1rev, skiprev=target)
349 358 if not collapsef:
350 359 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
351 360 editor=editor)
352 361 else:
353 362 # Skip commit if we are collapsing
354 363 repo.setparents(repo[p1].node())
355 364 newrev = None
356 365 # Update the state
357 366 if newrev is not None:
358 367 state[rev] = repo[newrev].rev()
359 368 else:
360 369 if not collapsef:
361 370 ui.note(_('no changes, revision %d skipped\n') % rev)
362 371 ui.debug('next revision set to %s\n' % p1)
363 372 skipped.add(rev)
364 373 state[rev] = p1
365 374
366 375 ui.progress(_('rebasing'), None)
367 376 ui.note(_('rebase merging completed\n'))
368 377
369 378 if collapsef and not keepopen:
370 379 p1, p2 = defineparents(repo, min(state), target,
371 380 state, targetancestors)
372 381 if collapsemsg:
373 382 commitmsg = collapsemsg
374 383 else:
375 384 commitmsg = 'Collapsed revision'
376 385 for rebased in state:
377 386 if rebased not in skipped and state[rebased] > nullmerge:
378 387 commitmsg += '\n* %s' % repo[rebased].description()
379 388 editor = cmdutil.commitforceeditor
380 389 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
381 390 extrafn=extrafn, editor=editor)
382 391 for oldrev in state.iterkeys():
383 392 if state[oldrev] > nullmerge:
384 393 state[oldrev] = newrev
385 394
386 395 if 'qtip' in repo.tags():
387 396 updatemq(repo, state, skipped, **opts)
388 397
389 398 if currentbookmarks:
390 399 # Nodeids are needed to reset bookmarks
391 400 nstate = {}
392 401 for k, v in state.iteritems():
393 402 if v > nullmerge:
394 403 nstate[repo[k].node()] = repo[v].node()
395 404 # XXX this is the same as dest.node() for the non-continue path --
396 405 # this should probably be cleaned up
397 406 targetnode = repo[target].node()
398 407
399 408 # restore original working directory
400 409 # (we do this before stripping)
401 410 newwd = state.get(originalwd, originalwd)
402 411 if newwd not in [c.rev() for c in repo[None].parents()]:
403 412 ui.note(_("update back to initial working directory parent\n"))
404 413 hg.updaterepo(repo, newwd, False)
405 414
406 415 if not keepf:
407 416 collapsedas = None
408 417 if collapsef:
409 418 collapsedas = newrev
410 419 clearrebased(ui, repo, state, skipped, collapsedas)
411 420
412 421 if currentbookmarks:
413 422 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
414 423 if activebookmark not in repo._bookmarks:
415 424 # active bookmark was divergent one and has been deleted
416 425 activebookmark = None
417 426
418 427 clearstatus(repo)
419 428 ui.note(_("rebase completed\n"))
420 429 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
421 430 if skipped:
422 431 ui.note(_("%d revisions have been skipped\n") % len(skipped))
423 432
424 433 if (activebookmark and
425 434 repo['.'].node() == repo._bookmarks[activebookmark]):
426 435 bookmarks.setcurrent(repo, activebookmark)
427 436
428 437 finally:
429 438 release(lock, wlock)
430 439
431 440 def externalparent(repo, state, targetancestors):
432 441 """Return the revision that should be used as the second parent
433 442 when the revisions in state is collapsed on top of targetancestors.
434 443 Abort if there is more than one parent.
435 444 """
436 445 parents = set()
437 446 source = min(state)
438 447 for rev in state:
439 448 if rev == source:
440 449 continue
441 450 for p in repo[rev].parents():
442 451 if (p.rev() not in state
443 452 and p.rev() not in targetancestors):
444 453 parents.add(p.rev())
445 454 if not parents:
446 455 return nullrev
447 456 if len(parents) == 1:
448 457 return parents.pop()
449 458 raise util.Abort(_('unable to collapse on top of %s, there is more '
450 459 'than one external parent: %s') %
451 460 (max(targetancestors),
452 461 ', '.join(str(p) for p in sorted(parents))))
453 462
454 463 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
455 464 'Commit the changes and store useful information in extra'
456 465 try:
457 466 repo.setparents(repo[p1].node(), repo[p2].node())
458 467 ctx = repo[rev]
459 468 if commitmsg is None:
460 469 commitmsg = ctx.description()
461 470 extra = {'rebase_source': ctx.hex()}
462 471 if extrafn:
463 472 extrafn(ctx, extra)
464 473 # Commit might fail if unresolved files exist
465 474 newrev = repo.commit(text=commitmsg, user=ctx.user(),
466 475 date=ctx.date(), extra=extra, editor=editor)
467 476 repo.dirstate.setbranch(repo[newrev].branch())
468 477 targetphase = max(ctx.phase(), phases.draft)
469 478 # retractboundary doesn't overwrite upper phase inherited from parent
470 479 newnode = repo[newrev].node()
471 480 if newnode:
472 481 phases.retractboundary(repo, targetphase, [newnode])
473 482 return newrev
474 483 except util.Abort:
475 484 # Invalidate the previous setparents
476 485 repo.dirstate.invalidate()
477 486 raise
478 487
479 488 def rebasenode(repo, rev, p1, state, collapse):
480 489 'Rebase a single revision'
481 490 # Merge phase
482 491 # Update to target and merge it with local
483 492 if repo['.'].rev() != repo[p1].rev():
484 493 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
485 494 merge.update(repo, p1, False, True, False)
486 495 else:
487 496 repo.ui.debug(" already in target\n")
488 497 repo.dirstate.write()
489 498 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
490 499 if repo[rev].rev() == repo[min(state)].rev():
491 500 # Case (1) initial changeset of a non-detaching rebase.
492 501 # Let the merge mechanism find the base itself.
493 502 base = None
494 503 elif not repo[rev].p2():
495 504 # Case (2) detaching the node with a single parent, use this parent
496 505 base = repo[rev].p1().node()
497 506 else:
498 507 # In case of merge, we need to pick the right parent as merge base.
499 508 #
500 509 # Imagine we have:
501 510 # - M: currently rebase revision in this step
502 511 # - A: one parent of M
503 512 # - B: second parent of M
504 513 # - D: destination of this merge step (p1 var)
505 514 #
506 515 # If we are rebasing on D, D is the successors of A or B. The right
507 516 # merge base is the one D succeed to. We pretend it is B for the rest
508 517 # of this comment
509 518 #
510 519 # If we pick B as the base, the merge involves:
511 520 # - changes from B to M (actual changeset payload)
512 521 # - changes from B to D (induced by rebase) as D is a rebased
513 522 # version of B)
514 523 # Which exactly represent the rebase operation.
515 524 #
516 525 # If we pick the A as the base, the merge involves
517 526 # - changes from A to M (actual changeset payload)
518 527 # - changes from A to D (with include changes between unrelated A and B
519 528 # plus changes induced by rebase)
520 529 # Which does not represent anything sensible and creates a lot of
521 530 # conflicts.
522 531 for p in repo[rev].parents():
523 532 if state.get(p.rev()) == repo[p1].rev():
524 533 base = p.node()
525 534 break
526 535 else: # fallback when base not found
527 536 base = None
528 537
529 538 # Raise because this function is called wrong (see issue 4106)
530 539 raise AssertionError('no base found to rebase on '
531 540 '(rebasenode called wrong)')
532 541 if base is not None:
533 542 repo.ui.debug(" detach base %d:%s\n" % (repo[base].rev(), repo[base]))
534 543 # When collapsing in-place, the parent is the common ancestor, we
535 544 # have to allow merging with it.
536 545 return merge.update(repo, rev, True, True, False, base, collapse)
537 546
538 547 def nearestrebased(repo, rev, state):
539 548 """return the nearest ancestors of rev in the rebase result"""
540 549 rebased = [r for r in state if state[r] > nullmerge]
541 550 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
542 551 if candidates:
543 552 return state[candidates[0]]
544 553 else:
545 554 return None
546 555
547 556 def defineparents(repo, rev, target, state, targetancestors):
548 557 'Return the new parent relationship of the revision that will be rebased'
549 558 parents = repo[rev].parents()
550 559 p1 = p2 = nullrev
551 560
552 561 P1n = parents[0].rev()
553 562 if P1n in targetancestors:
554 563 p1 = target
555 564 elif P1n in state:
556 565 if state[P1n] == nullmerge:
557 566 p1 = target
558 567 elif state[P1n] == revignored:
559 568 p1 = nearestrebased(repo, P1n, state)
560 569 if p1 is None:
561 570 p1 = target
562 571 else:
563 572 p1 = state[P1n]
564 573 else: # P1n external
565 574 p1 = target
566 575 p2 = P1n
567 576
568 577 if len(parents) == 2 and parents[1].rev() not in targetancestors:
569 578 P2n = parents[1].rev()
570 579 # interesting second parent
571 580 if P2n in state:
572 581 if p1 == target: # P1n in targetancestors or external
573 582 p1 = state[P2n]
574 583 elif state[P2n] == revignored:
575 584 p2 = nearestrebased(repo, P2n, state)
576 585 if p2 is None:
577 586 # no ancestors rebased yet, detach
578 587 p2 = target
579 588 else:
580 589 p2 = state[P2n]
581 590 else: # P2n external
582 591 if p2 != nullrev: # P1n external too => rev is a merged revision
583 592 raise util.Abort(_('cannot use revision %d as base, result '
584 593 'would have 3 parents') % rev)
585 594 p2 = P2n
586 595 repo.ui.debug(" future parents are %d and %d\n" %
587 596 (repo[p1].rev(), repo[p2].rev()))
588 597 return p1, p2
589 598
590 599 def isagitpatch(repo, patchname):
591 600 'Return true if the given patch is in git format'
592 601 mqpatch = os.path.join(repo.mq.path, patchname)
593 602 for line in patch.linereader(file(mqpatch, 'rb')):
594 603 if line.startswith('diff --git'):
595 604 return True
596 605 return False
597 606
598 607 def updatemq(repo, state, skipped, **opts):
599 608 'Update rebased mq patches - finalize and then import them'
600 609 mqrebase = {}
601 610 mq = repo.mq
602 611 original_series = mq.fullseries[:]
603 612 skippedpatches = set()
604 613
605 614 for p in mq.applied:
606 615 rev = repo[p.node].rev()
607 616 if rev in state:
608 617 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
609 618 (rev, p.name))
610 619 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
611 620 else:
612 621 # Applied but not rebased, not sure this should happen
613 622 skippedpatches.add(p.name)
614 623
615 624 if mqrebase:
616 625 mq.finish(repo, mqrebase.keys())
617 626
618 627 # We must start import from the newest revision
619 628 for rev in sorted(mqrebase, reverse=True):
620 629 if rev not in skipped:
621 630 name, isgit = mqrebase[rev]
622 631 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
623 632 mq.qimport(repo, (), patchname=name, git=isgit,
624 633 rev=[str(state[rev])])
625 634 else:
626 635 # Rebased and skipped
627 636 skippedpatches.add(mqrebase[rev][0])
628 637
629 638 # Patches were either applied and rebased and imported in
630 639 # order, applied and removed or unapplied. Discard the removed
631 640 # ones while preserving the original series order and guards.
632 641 newseries = [s for s in original_series
633 642 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
634 643 mq.fullseries[:] = newseries
635 644 mq.seriesdirty = True
636 645 mq.savedirty()
637 646
638 647 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
639 648 'Move bookmarks to their correct changesets, and delete divergent ones'
640 649 marks = repo._bookmarks
641 650 for k, v in originalbookmarks.iteritems():
642 651 if v in nstate:
643 652 # update the bookmarks for revs that have moved
644 653 marks[k] = nstate[v]
645 654 bookmarks.deletedivergent(repo, [targetnode], k)
646 655
647 656 marks.write()
648 657
649 658 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
650 659 external, activebookmark):
651 660 'Store the current status to allow recovery'
652 661 f = repo.opener("rebasestate", "w")
653 662 f.write(repo[originalwd].hex() + '\n')
654 663 f.write(repo[target].hex() + '\n')
655 664 f.write(repo[external].hex() + '\n')
656 665 f.write('%d\n' % int(collapse))
657 666 f.write('%d\n' % int(keep))
658 667 f.write('%d\n' % int(keepbranches))
659 668 f.write('%s\n' % (activebookmark or ''))
660 669 for d, v in state.iteritems():
661 670 oldrev = repo[d].hex()
662 671 if v > nullmerge:
663 672 newrev = repo[v].hex()
664 673 else:
665 674 newrev = v
666 675 f.write("%s:%s\n" % (oldrev, newrev))
667 676 f.close()
668 677 repo.ui.debug('rebase status stored\n')
669 678
670 679 def clearstatus(repo):
671 680 'Remove the status files'
672 681 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
673 682
674 683 def restorestatus(repo):
675 684 'Restore a previously stored status'
676 685 try:
677 686 keepbranches = None
678 687 target = None
679 688 collapse = False
680 689 external = nullrev
681 690 activebookmark = None
682 691 state = {}
683 692 f = repo.opener("rebasestate")
684 693 for i, l in enumerate(f.read().splitlines()):
685 694 if i == 0:
686 695 originalwd = repo[l].rev()
687 696 elif i == 1:
688 697 target = repo[l].rev()
689 698 elif i == 2:
690 699 external = repo[l].rev()
691 700 elif i == 3:
692 701 collapse = bool(int(l))
693 702 elif i == 4:
694 703 keep = bool(int(l))
695 704 elif i == 5:
696 705 keepbranches = bool(int(l))
697 706 elif i == 6 and not (len(l) == 81 and ':' in l):
698 707 # line 6 is a recent addition, so for backwards compatibility
699 708 # check that the line doesn't look like the oldrev:newrev lines
700 709 activebookmark = l
701 710 else:
702 711 oldrev, newrev = l.split(':')
703 712 if newrev in (str(nullmerge), str(revignored)):
704 713 state[repo[oldrev].rev()] = int(newrev)
705 714 else:
706 715 state[repo[oldrev].rev()] = repo[newrev].rev()
707 716
708 717 if keepbranches is None:
709 718 raise util.Abort(_('.hg/rebasestate is incomplete'))
710 719
711 720 skipped = set()
712 721 # recompute the set of skipped revs
713 722 if not collapse:
714 723 seen = set([target])
715 724 for old, new in sorted(state.items()):
716 725 if new != nullrev and new in seen:
717 726 skipped.add(old)
718 727 seen.add(new)
719 728 repo.ui.debug('computed skipped revs: %s\n' %
720 729 (' '.join(str(r) for r in sorted(skipped)) or None))
721 730 repo.ui.debug('rebase status resumed\n')
722 731 return (originalwd, target, state, skipped,
723 732 collapse, keep, keepbranches, external, activebookmark)
724 733 except IOError, err:
725 734 if err.errno != errno.ENOENT:
726 735 raise
727 736 raise util.Abort(_('no rebase in progress'))
728 737
729 738 def inrebase(repo, originalwd, state):
730 739 '''check whether the working dir is in an interrupted rebase'''
731 740 parents = [p.rev() for p in repo.parents()]
732 741 if originalwd in parents:
733 742 return True
734 743
735 744 for newrev in state.itervalues():
736 745 if newrev in parents:
737 746 return True
738 747
739 748 return False
740 749
741 750 def abort(repo, originalwd, target, state):
742 751 'Restore the repository to its original state'
743 752 dstates = [s for s in state.values() if s > nullrev]
744 753 immutable = [d for d in dstates if not repo[d].mutable()]
745 754 cleanup = True
746 755 if immutable:
747 756 repo.ui.warn(_("warning: can't clean up immutable changesets %s\n")
748 757 % ', '.join(str(repo[r]) for r in immutable),
749 758 hint=_('see hg help phases for details'))
750 759 cleanup = False
751 760
752 761 descendants = set()
753 762 if dstates:
754 763 descendants = set(repo.changelog.descendants(dstates))
755 764 if descendants - set(dstates):
756 765 repo.ui.warn(_("warning: new changesets detected on target branch, "
757 766 "can't strip\n"))
758 767 cleanup = False
759 768
760 769 if cleanup:
761 770 # Update away from the rebase if necessary
762 771 if inrebase(repo, originalwd, state):
763 772 merge.update(repo, repo[originalwd].rev(), False, True, False)
764 773
765 774 # Strip from the first rebased revision
766 775 rebased = filter(lambda x: x > -1 and x != target, state.values())
767 776 if rebased:
768 777 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
769 778 # no backup of rebased cset versions needed
770 779 repair.strip(repo.ui, repo, strippoints)
771 780
772 781 clearstatus(repo)
773 782 repo.ui.warn(_('rebase aborted\n'))
774 783 return 0
775 784
776 785 def buildstate(repo, dest, rebaseset, collapse):
777 786 '''Define which revisions are going to be rebased and where
778 787
779 788 repo: repo
780 789 dest: context
781 790 rebaseset: set of rev
782 791 '''
783 792
784 793 # This check isn't strictly necessary, since mq detects commits over an
785 794 # applied patch. But it prevents messing up the working directory when
786 795 # a partially completed rebase is blocked by mq.
787 796 if 'qtip' in repo.tags() and (dest.node() in
788 797 [s.node for s in repo.mq.applied]):
789 798 raise util.Abort(_('cannot rebase onto an applied mq patch'))
790 799
791 800 roots = list(repo.set('roots(%ld)', rebaseset))
792 801 if not roots:
793 802 raise util.Abort(_('no matching revisions'))
794 803 roots.sort()
795 804 state = {}
796 805 detachset = set()
797 806 for root in roots:
798 807 commonbase = root.ancestor(dest)
799 808 if commonbase == root:
800 809 raise util.Abort(_('source is ancestor of destination'))
801 810 if commonbase == dest:
802 811 samebranch = root.branch() == dest.branch()
803 812 if not collapse and samebranch and root in dest.children():
804 813 repo.ui.debug('source is a child of destination\n')
805 814 return None
806 815
807 816 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, root))
808 817 state.update(dict.fromkeys(rebaseset, nullrev))
809 818 # Rebase tries to turn <dest> into a parent of <root> while
810 819 # preserving the number of parents of rebased changesets:
811 820 #
812 821 # - A changeset with a single parent will always be rebased as a
813 822 # changeset with a single parent.
814 823 #
815 824 # - A merge will be rebased as merge unless its parents are both
816 825 # ancestors of <dest> or are themselves in the rebased set and
817 826 # pruned while rebased.
818 827 #
819 828 # If one parent of <root> is an ancestor of <dest>, the rebased
820 829 # version of this parent will be <dest>. This is always true with
821 830 # --base option.
822 831 #
823 832 # Otherwise, we need to *replace* the original parents with
824 833 # <dest>. This "detaches" the rebased set from its former location
825 834 # and rebases it onto <dest>. Changes introduced by ancestors of
826 835 # <root> not common with <dest> (the detachset, marked as
827 836 # nullmerge) are "removed" from the rebased changesets.
828 837 #
829 838 # - If <root> has a single parent, set it to <dest>.
830 839 #
831 840 # - If <root> is a merge, we cannot decide which parent to
832 841 # replace, the rebase operation is not clearly defined.
833 842 #
834 843 # The table below sums up this behavior:
835 844 #
836 845 # +------------------+----------------------+-------------------------+
837 846 # | | one parent | merge |
838 847 # +------------------+----------------------+-------------------------+
839 848 # | parent in | new parent is <dest> | parents in ::<dest> are |
840 849 # | ::<dest> | | remapped to <dest> |
841 850 # +------------------+----------------------+-------------------------+
842 851 # | unrelated source | new parent is <dest> | ambiguous, abort |
843 852 # +------------------+----------------------+-------------------------+
844 853 #
845 854 # The actual abort is handled by `defineparents`
846 855 if len(root.parents()) <= 1:
847 856 # ancestors of <root> not ancestors of <dest>
848 857 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
849 858 [root.rev()]))
850 859 for r in detachset:
851 860 if r not in state:
852 861 state[r] = nullmerge
853 862 if len(roots) > 1:
854 863 # If we have multiple roots, we may have "hole" in the rebase set.
855 864 # Rebase roots that descend from those "hole" should not be detached as
856 865 # other root are. We use the special `revignored` to inform rebase that
857 866 # the revision should be ignored but that `defineparents` should search
858 867 # a rebase destination that make sense regarding rebased topology.
859 868 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
860 869 for ignored in set(rebasedomain) - set(rebaseset):
861 870 state[ignored] = revignored
862 871 return repo['.'].rev(), dest.rev(), state
863 872
864 873 def clearrebased(ui, repo, state, skipped, collapsedas=None):
865 874 """dispose of rebased revision at the end of the rebase
866 875
867 876 If `collapsedas` is not None, the rebase was a collapse whose result if the
868 877 `collapsedas` node."""
869 878 if obsolete._enabled:
870 879 markers = []
871 880 for rev, newrev in sorted(state.items()):
872 881 if newrev >= 0:
873 882 if rev in skipped:
874 883 succs = ()
875 884 elif collapsedas is not None:
876 885 succs = (repo[collapsedas],)
877 886 else:
878 887 succs = (repo[newrev],)
879 888 markers.append((repo[rev], succs))
880 889 if markers:
881 890 obsolete.createmarkers(repo, markers)
882 891 else:
883 892 rebased = [rev for rev in state if state[rev] > nullmerge]
884 893 if rebased:
885 894 stripped = []
886 895 for root in repo.set('roots(%ld)', rebased):
887 896 if set(repo.changelog.descendants([root.rev()])) - set(state):
888 897 ui.warn(_("warning: new changesets detected "
889 898 "on source branch, not stripping\n"))
890 899 else:
891 900 stripped.append(root.node())
892 901 if stripped:
893 902 # backup the old csets by default
894 903 repair.strip(ui, repo, stripped, "all")
895 904
896 905
897 906 def pullrebase(orig, ui, repo, *args, **opts):
898 907 'Call rebase after pull if the latter has been invoked with --rebase'
899 908 if opts.get('rebase'):
900 909 if opts.get('update'):
901 910 del opts['update']
902 911 ui.debug('--update and --rebase are not compatible, ignoring '
903 912 'the update flag\n')
904 913
905 914 movemarkfrom = repo['.'].node()
906 915 revsprepull = len(repo)
907 916 origpostincoming = commands.postincoming
908 917 def _dummy(*args, **kwargs):
909 918 pass
910 919 commands.postincoming = _dummy
911 920 try:
912 921 orig(ui, repo, *args, **opts)
913 922 finally:
914 923 commands.postincoming = origpostincoming
915 924 revspostpull = len(repo)
916 925 if revspostpull > revsprepull:
917 926 # --rev option from pull conflict with rebase own --rev
918 927 # dropping it
919 928 if 'rev' in opts:
920 929 del opts['rev']
921 930 rebase(ui, repo, **opts)
922 931 branch = repo[None].branch()
923 932 dest = repo[branch].rev()
924 933 if dest != repo['.'].rev():
925 934 # there was nothing to rebase we force an update
926 935 hg.update(repo, dest)
927 936 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
928 937 ui.status(_("updating bookmark %s\n")
929 938 % repo._bookmarkcurrent)
930 939 else:
931 940 if opts.get('tool'):
932 941 raise util.Abort(_('--tool can only be used with --rebase'))
933 942 orig(ui, repo, *args, **opts)
934 943
935 944 def summaryhook(ui, repo):
936 945 if not os.path.exists(repo.join('rebasestate')):
937 946 return
938 947 try:
939 948 state = restorestatus(repo)[2]
940 949 except error.RepoLookupError:
941 950 # i18n: column positioning for "hg summary"
942 951 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
943 952 ui.write(msg)
944 953 return
945 954 numrebased = len([i for i in state.itervalues() if i != -1])
946 955 # i18n: column positioning for "hg summary"
947 956 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
948 957 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
949 958 ui.label(_('%d remaining'), 'rebase.remaining') %
950 959 (len(state) - numrebased)))
951 960
952 961 def uisetup(ui):
953 962 'Replace pull with a decorator to provide --rebase option'
954 963 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
955 964 entry[1].append(('', 'rebase', None,
956 965 _("rebase working directory to branch head")))
957 966 entry[1].append(('t', 'tool', '',
958 967 _("specify merge tool for rebase")))
959 968 cmdutil.summaryhooks.add('rebase', summaryhook)
960 969 cmdutil.unfinishedstates.append(
961 970 ['rebasestate', False, False, _('rebase in progress'),
962 971 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
@@ -1,242 +1,323 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > rebase=
4 4 >
5 5 > [alias]
6 6 > tlog = log --template "{rev}: '{desc}' {branches}\n"
7 7 > tglog = tlog --graph
8 8 > EOF
9 9
10 10
11 11 $ hg init a
12 12 $ cd a
13 13
14 14 $ mkdir d
15 15 $ echo a > a
16 16 $ hg ci -Am A
17 17 adding a
18 18
19 19 $ echo b > d/b
20 20 $ hg ci -Am B
21 21 adding d/b
22 22
23 23 $ hg mv d d-renamed
24 24 moving d/b to d-renamed/b (glob)
25 25 $ hg ci -m 'rename B'
26 26
27 27 $ hg up -q -C 1
28 28
29 29 $ hg mv a a-renamed
30 30 $ echo x > d/x
31 31 $ hg add d/x
32 32
33 33 $ hg ci -m 'rename A'
34 34 created new head
35 35
36 36 $ hg tglog
37 37 @ 3: 'rename A'
38 38 |
39 39 | o 2: 'rename B'
40 40 |/
41 41 o 1: 'B'
42 42 |
43 43 o 0: 'A'
44 44
45 45
46 46 Rename is tracked:
47 47
48 48 $ hg tlog -p --git -r tip
49 49 3: 'rename A'
50 50 diff --git a/a b/a-renamed
51 51 rename from a
52 52 rename to a-renamed
53 53 diff --git a/d/x b/d/x
54 54 new file mode 100644
55 55 --- /dev/null
56 56 +++ b/d/x
57 57 @@ -0,0 +1,1 @@
58 58 +x
59 59
60 60 Rebase the revision containing the rename:
61 61
62 62 $ hg rebase -s 3 -d 2
63 63 saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
64 64
65 65 $ hg tglog
66 66 @ 3: 'rename A'
67 67 |
68 68 o 2: 'rename B'
69 69 |
70 70 o 1: 'B'
71 71 |
72 72 o 0: 'A'
73 73
74 74
75 75 Rename is not lost:
76 76
77 77 $ hg tlog -p --git -r tip
78 78 3: 'rename A'
79 79 diff --git a/a b/a-renamed
80 80 rename from a
81 81 rename to a-renamed
82 82 diff --git a/d-renamed/x b/d-renamed/x
83 83 new file mode 100644
84 84 --- /dev/null
85 85 +++ b/d-renamed/x
86 86 @@ -0,0 +1,1 @@
87 87 +x
88 88
89 89
90 90 Rebased revision does not contain information about b (issue3739)
91 91
92 92 $ hg log -r 3 --debug
93 93 changeset: 3:032a9b75e83bff1dcfb6cbfa4ef50a704bf1b569
94 94 tag: tip
95 95 phase: draft
96 96 parent: 2:220d0626d185f372d9d8f69d9c73b0811d7725f7
97 97 parent: -1:0000000000000000000000000000000000000000
98 98 manifest: 3:035d66b27a1b06b2d12b46d41a39adb7a200c370
99 99 user: test
100 100 date: Thu Jan 01 00:00:00 1970 +0000
101 101 files+: a-renamed d-renamed/x
102 102 files-: a
103 103 extra: branch=default
104 104 extra: rebase_source=73a3ee40125d6f0f347082e5831ceccb3f005f8a
105 105 description:
106 106 rename A
107 107
108 108
109 109
110 110 $ cd ..
111 111
112 112
113 113 $ hg init b
114 114 $ cd b
115 115
116 116 $ echo a > a
117 117 $ hg ci -Am A
118 118 adding a
119 119
120 120 $ echo b > b
121 121 $ hg ci -Am B
122 122 adding b
123 123
124 124 $ hg cp b b-copied
125 125 $ hg ci -Am 'copy B'
126 126
127 127 $ hg up -q -C 1
128 128
129 129 $ hg cp a a-copied
130 130 $ hg ci -m 'copy A'
131 131 created new head
132 132
133 133 $ hg tglog
134 134 @ 3: 'copy A'
135 135 |
136 136 | o 2: 'copy B'
137 137 |/
138 138 o 1: 'B'
139 139 |
140 140 o 0: 'A'
141 141
142 142 Copy is tracked:
143 143
144 144 $ hg tlog -p --git -r tip
145 145 3: 'copy A'
146 146 diff --git a/a b/a-copied
147 147 copy from a
148 148 copy to a-copied
149 149
150 150 Rebase the revision containing the copy:
151 151
152 152 $ hg rebase -s 3 -d 2
153 153 saved backup bundle to $TESTTMP/b/.hg/strip-backup/*-backup.hg (glob)
154 154
155 155 $ hg tglog
156 156 @ 3: 'copy A'
157 157 |
158 158 o 2: 'copy B'
159 159 |
160 160 o 1: 'B'
161 161 |
162 162 o 0: 'A'
163 163
164 164
165 165 Copy is not lost:
166 166
167 167 $ hg tlog -p --git -r tip
168 168 3: 'copy A'
169 169 diff --git a/a b/a-copied
170 170 copy from a
171 171 copy to a-copied
172 172
173 173
174 174 Rebased revision does not contain information about b (issue3739)
175 175
176 176 $ hg log -r 3 --debug
177 177 changeset: 3:98f6e6dbf45ab54079c2237fbd11066a5c41a11d
178 178 tag: tip
179 179 phase: draft
180 180 parent: 2:39e588434882ff77d01229d169cdc77f29e8855e
181 181 parent: -1:0000000000000000000000000000000000000000
182 182 manifest: 3:2232f329d66fffe3930d43479ae624f66322b04d
183 183 user: test
184 184 date: Thu Jan 01 00:00:00 1970 +0000
185 185 files+: a-copied
186 186 extra: branch=default
187 187 extra: rebase_source=0a8162ff18a8900df8df8ef7ac0046955205613e
188 188 description:
189 189 copy A
190 190
191 191
192 192
193 193 $ cd ..
194 194
195 195
196 196 Test rebase across repeating renames:
197 197
198 198 $ hg init repo
199 199
200 200 $ cd repo
201 201
202 202 $ echo testing > file1.txt
203 203 $ hg add file1.txt
204 204 $ hg ci -m "Adding file1"
205 205
206 206 $ hg rename file1.txt file2.txt
207 207 $ hg ci -m "Rename file1 to file2"
208 208
209 209 $ echo Unrelated change > unrelated.txt
210 210 $ hg add unrelated.txt
211 211 $ hg ci -m "Unrelated change"
212 212
213 213 $ hg rename file2.txt file1.txt
214 214 $ hg ci -m "Rename file2 back to file1"
215 215
216 216 $ hg update -r -2
217 217 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
218 218
219 219 $ echo Another unrelated change >> unrelated.txt
220 220 $ hg ci -m "Another unrelated change"
221 221 created new head
222 222
223 223 $ hg tglog
224 224 @ 4: 'Another unrelated change'
225 225 |
226 226 | o 3: 'Rename file2 back to file1'
227 227 |/
228 228 o 2: 'Unrelated change'
229 229 |
230 230 o 1: 'Rename file1 to file2'
231 231 |
232 232 o 0: 'Adding file1'
233 233
234 234
235 235 $ hg rebase -s 4 -d 3
236 236 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-backup.hg (glob)
237 237
238 238 $ hg diff --stat -c .
239 239 unrelated.txt | 1 +
240 240 1 files changed, 1 insertions(+), 0 deletions(-)
241 241
242 242 $ cd ..
243
244 Verify that copies get preserved (issue4192).
245 $ hg init copy-gets-preserved
246 $ cd copy-gets-preserved
247
248 $ echo a > a
249 $ hg add a
250 $ hg commit --message "File a created"
251 $ hg copy a b
252 $ echo b > b
253 $ hg commit --message "File b created as copy of a and modified"
254 $ hg copy b c
255 $ echo c > c
256 $ hg commit --message "File c created as copy of b and modified"
257 $ hg copy c d
258 $ echo d > d
259 $ hg commit --message "File d created as copy of c and modified"
260
261 Note that there are four entries in the log for d
262 $ hg tglog --follow d
263 @ 3: 'File d created as copy of c and modified'
264 |
265 o 2: 'File c created as copy of b and modified'
266 |
267 o 1: 'File b created as copy of a and modified'
268 |
269 o 0: 'File a created'
270
271 Update back to before we performed copies, and inject an unrelated change.
272 $ hg update 0
273 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
274
275 $ echo unrelated > unrelated
276 $ hg add unrelated
277 $ hg commit --message "Unrelated file created"
278 created new head
279 $ hg update 4
280 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
281
282 Rebase the copies on top of the unrelated change.
283 $ hg rebase --source 1 --dest 4
284 saved backup bundle to $TESTTMP/copy-gets-preserved/.hg/*.hg (glob)
285 $ hg update 4
286 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
287
288 There should still be four entries in the log for d
289 $ hg tglog --follow d
290 @ 4: 'File d created as copy of c and modified'
291 |
292 o 3: 'File c created as copy of b and modified'
293 |
294 o 2: 'File b created as copy of a and modified'
295 |
296 o 0: 'File a created'
297
298 Same steps as above, but with --collapse on rebase to make sure the
299 copy records collapse correctly.
300 $ hg co 1
301 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
302 $ echo more >> unrelated
303 $ hg ci -m 'unrelated commit is unrelated'
304 created new head
305 $ hg rebase -s 2 --dest 5 --collapse
306 merging b and c to c
307 merging c and d to d
308 saved backup bundle to $TESTTMP/copy-gets-preserved/.hg/*.hg (glob)
309 $ hg co tip
310 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
311
312 This should show both revision 3 and 0 since 'd' was transitively a
313 copy of 'a'.
314
315 $ hg tglog --follow d
316 @ 3: 'Collapsed revision
317 | * File b created as copy of a and modified
318 | * File c created as copy of b and modified
319 | * File d created as copy of c and modified'
320 o 0: 'File a created'
321
322
323 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now