##// END OF EJS Templates
rebase: don't mark file as removed if missing in parent's manifest (issue2725)
Stefano Tortarolo -
r13778:46c30432 default
parent child Browse files
Show More
@@ -1,600 +1,600 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
18 18 from mercurial import extensions, ancestor, copies, patch
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
27 27 def rebase(ui, repo, **opts):
28 28 """move changeset (and descendants) to a different branch
29 29
30 30 Rebase uses repeated merging to graft changesets from one part of
31 31 history (the source) onto another (the destination). This can be
32 32 useful for linearizing *local* changes relative to a master
33 33 development tree.
34 34
35 35 You should not rebase changesets that have already been shared
36 36 with others. Doing so will force everybody else to perform the
37 37 same rebase or they will end up with duplicated changesets after
38 38 pulling in your rebased changesets.
39 39
40 40 If you don't specify a destination changeset (``-d/--dest``),
41 41 rebase uses the tipmost head of the current named branch as the
42 42 destination. (The destination changeset is not modified by
43 43 rebasing, but new changesets are added as its descendants.)
44 44
45 45 You can specify which changesets to rebase in two ways: as a
46 46 "source" changeset or as a "base" changeset. Both are shorthand
47 47 for a topologically related set of changesets (the "source
48 48 branch"). If you specify source (``-s/--source``), rebase will
49 49 rebase that changeset and all of its descendants onto dest. If you
50 50 specify base (``-b/--base``), rebase will select ancestors of base
51 51 back to but not including the common ancestor with dest. Thus,
52 52 ``-b`` is less precise but more convenient than ``-s``: you can
53 53 specify any changeset in the source branch, and rebase will select
54 54 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
55 55 uses the parent of the working directory as the base.
56 56
57 57 By default, rebase recreates the changesets in the source branch
58 58 as descendants of dest and then destroys the originals. Use
59 59 ``--keep`` to preserve the original source changesets. Some
60 60 changesets in the source branch (e.g. merges from the destination
61 61 branch) may be dropped if they no longer contribute any change.
62 62
63 63 One result of the rules for selecting the destination changeset
64 64 and source branch is that, unlike ``merge``, rebase will do
65 65 nothing if you are at the latest (tipmost) head of a named branch
66 66 with two heads. You need to explicitly specify source and/or
67 67 destination (or ``update`` to the other head, if it's the head of
68 68 the intended source branch).
69 69
70 70 If a rebase is interrupted to manually resolve a merge, it can be
71 71 continued with --continue/-c or aborted with --abort/-a.
72 72
73 73 Returns 0 on success, 1 if nothing to rebase.
74 74 """
75 75 originalwd = target = None
76 76 external = nullrev
77 77 state = {}
78 78 skipped = set()
79 79 targetancestors = set()
80 80
81 81 lock = wlock = None
82 82 try:
83 83 lock = repo.lock()
84 84 wlock = repo.wlock()
85 85
86 86 # Validate input and define rebasing points
87 87 destf = opts.get('dest', None)
88 88 srcf = opts.get('source', None)
89 89 basef = opts.get('base', None)
90 90 contf = opts.get('continue')
91 91 abortf = opts.get('abort')
92 92 collapsef = opts.get('collapse', False)
93 93 collapsemsg = cmdutil.logmessage(opts)
94 94 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
95 95 keepf = opts.get('keep', False)
96 96 keepbranchesf = opts.get('keepbranches', False)
97 97 detachf = opts.get('detach', False)
98 98 # keepopen is not meant for use on the command line, but by
99 99 # other extensions
100 100 keepopen = opts.get('keepopen', False)
101 101
102 102 if collapsemsg and not collapsef:
103 103 raise util.Abort(
104 104 _('message can only be specified with collapse'))
105 105
106 106 if contf or abortf:
107 107 if contf and abortf:
108 108 raise util.Abort(_('cannot use both abort and continue'))
109 109 if collapsef:
110 110 raise util.Abort(
111 111 _('cannot use collapse with continue or abort'))
112 112 if detachf:
113 113 raise util.Abort(_('cannot use detach with continue or abort'))
114 114 if srcf or basef or destf:
115 115 raise util.Abort(
116 116 _('abort and continue do not allow specifying revisions'))
117 117
118 118 (originalwd, target, state, skipped, collapsef, keepf,
119 119 keepbranchesf, external) = restorestatus(repo)
120 120 if abortf:
121 121 return abort(repo, originalwd, target, state)
122 122 else:
123 123 if srcf and basef:
124 124 raise util.Abort(_('cannot specify both a '
125 125 'revision and a base'))
126 126 if detachf:
127 127 if not srcf:
128 128 raise util.Abort(
129 129 _('detach requires a revision to be specified'))
130 130 if basef:
131 131 raise util.Abort(_('cannot specify a base with detach'))
132 132
133 133 cmdutil.bail_if_changed(repo)
134 134 result = buildstate(repo, destf, srcf, basef, detachf)
135 135 if not result:
136 136 # Empty state built, nothing to rebase
137 137 ui.status(_('nothing to rebase\n'))
138 138 return 1
139 139 else:
140 140 originalwd, target, state = result
141 141 if collapsef:
142 142 targetancestors = set(repo.changelog.ancestors(target))
143 143 external = checkexternal(repo, state, targetancestors)
144 144
145 145 if keepbranchesf:
146 146 assert not extrafn, 'cannot use both keepbranches and extrafn'
147 147 def extrafn(ctx, extra):
148 148 extra['branch'] = ctx.branch()
149 149
150 150 # Rebase
151 151 if not targetancestors:
152 152 targetancestors = set(repo.changelog.ancestors(target))
153 153 targetancestors.add(target)
154 154
155 155 sortedstate = sorted(state)
156 156 total = len(sortedstate)
157 157 pos = 0
158 158 for rev in sortedstate:
159 159 pos += 1
160 160 if state[rev] == -1:
161 161 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
162 162 _('changesets'), total)
163 163 storestatus(repo, originalwd, target, state, collapsef, keepf,
164 164 keepbranchesf, external)
165 165 p1, p2 = defineparents(repo, rev, target, state,
166 166 targetancestors)
167 167 if len(repo.parents()) == 2:
168 168 repo.ui.debug('resuming interrupted rebase\n')
169 169 else:
170 170 stats = rebasenode(repo, rev, p1, p2, state)
171 171 if stats and stats[3] > 0:
172 172 raise util.Abort(_('unresolved conflicts (see hg '
173 173 'resolve, then hg rebase --continue)'))
174 174 updatedirstate(repo, rev, target, p2)
175 175 if not collapsef:
176 176 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn)
177 177 else:
178 178 # Skip commit if we are collapsing
179 179 repo.dirstate.setparents(repo[p1].node())
180 180 newrev = None
181 181 # Update the state
182 182 if newrev is not None:
183 183 state[rev] = repo[newrev].rev()
184 184 else:
185 185 if not collapsef:
186 186 ui.note(_('no changes, revision %d skipped\n') % rev)
187 187 ui.debug('next revision set to %s\n' % p1)
188 188 skipped.add(rev)
189 189 state[rev] = p1
190 190
191 191 ui.progress(_('rebasing'), None)
192 192 ui.note(_('rebase merging completed\n'))
193 193
194 194 if collapsef and not keepopen:
195 195 p1, p2 = defineparents(repo, min(state), target,
196 196 state, targetancestors)
197 197 if collapsemsg:
198 198 commitmsg = collapsemsg
199 199 else:
200 200 commitmsg = 'Collapsed revision'
201 201 for rebased in state:
202 202 if rebased not in skipped and state[rebased] != nullmerge:
203 203 commitmsg += '\n* %s' % repo[rebased].description()
204 204 commitmsg = ui.edit(commitmsg, repo.ui.username())
205 205 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
206 206 extrafn=extrafn)
207 207
208 208 if 'qtip' in repo.tags():
209 209 updatemq(repo, state, skipped, **opts)
210 210
211 211 if not keepf:
212 212 # Remove no more useful revisions
213 213 rebased = [rev for rev in state if state[rev] != nullmerge]
214 214 if rebased:
215 215 if set(repo.changelog.descendants(min(rebased))) - set(state):
216 216 ui.warn(_("warning: new changesets detected "
217 217 "on source branch, not stripping\n"))
218 218 else:
219 219 # backup the old csets by default
220 220 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
221 221
222 222 clearstatus(repo)
223 223 ui.note(_("rebase completed\n"))
224 224 if os.path.exists(repo.sjoin('undo')):
225 225 util.unlinkpath(repo.sjoin('undo'))
226 226 if skipped:
227 227 ui.note(_("%d revisions have been skipped\n") % len(skipped))
228 228 finally:
229 229 release(lock, wlock)
230 230
231 231 def rebasemerge(repo, rev, first=False):
232 232 'return the correct ancestor'
233 233 oldancestor = ancestor.ancestor
234 234
235 235 def newancestor(a, b, pfunc):
236 236 if b == rev:
237 237 return repo[rev].parents()[0].rev()
238 238 return oldancestor(a, b, pfunc)
239 239
240 240 if not first:
241 241 ancestor.ancestor = newancestor
242 242 else:
243 243 repo.ui.debug("first revision, do not change ancestor\n")
244 244 try:
245 245 stats = merge.update(repo, rev, True, True, False)
246 246 return stats
247 247 finally:
248 248 ancestor.ancestor = oldancestor
249 249
250 250 def checkexternal(repo, state, targetancestors):
251 251 """Check whether one or more external revisions need to be taken in
252 252 consideration. In the latter case, abort.
253 253 """
254 254 external = nullrev
255 255 source = min(state)
256 256 for rev in state:
257 257 if rev == source:
258 258 continue
259 259 # Check externals and fail if there are more than one
260 260 for p in repo[rev].parents():
261 261 if (p.rev() not in state
262 262 and p.rev() not in targetancestors):
263 263 if external != nullrev:
264 264 raise util.Abort(_('unable to collapse, there is more '
265 265 'than one external parent'))
266 266 external = p.rev()
267 267 return external
268 268
269 269 def updatedirstate(repo, rev, p1, p2):
270 270 """Keep track of renamed files in the revision that is going to be rebased
271 271 """
272 272 # Here we simulate the copies and renames in the source changeset
273 273 cop, diver = copies.copies(repo, repo[rev], repo[p1], repo[p2], True)
274 274 m1 = repo[rev].manifest()
275 275 m2 = repo[p1].manifest()
276 276 for k, v in cop.iteritems():
277 277 if k in m1:
278 278 if v in m1 or v in m2:
279 279 repo.dirstate.copy(v, k)
280 if v in m2 and v not in m1:
280 if v in m2 and v not in m1 and k in m2:
281 281 repo.dirstate.remove(v)
282 282
283 283 def concludenode(repo, rev, p1, p2, commitmsg=None, extrafn=None):
284 284 'Commit the changes and store useful information in extra'
285 285 try:
286 286 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
287 287 ctx = repo[rev]
288 288 if commitmsg is None:
289 289 commitmsg = ctx.description()
290 290 extra = {'rebase_source': ctx.hex()}
291 291 if extrafn:
292 292 extrafn(ctx, extra)
293 293 # Commit might fail if unresolved files exist
294 294 newrev = repo.commit(text=commitmsg, user=ctx.user(),
295 295 date=ctx.date(), extra=extra)
296 296 repo.dirstate.setbranch(repo[newrev].branch())
297 297 return newrev
298 298 except util.Abort:
299 299 # Invalidate the previous setparents
300 300 repo.dirstate.invalidate()
301 301 raise
302 302
303 303 def rebasenode(repo, rev, p1, p2, state):
304 304 'Rebase a single revision'
305 305 # Merge phase
306 306 # Update to target and merge it with local
307 307 if repo['.'].rev() != repo[p1].rev():
308 308 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
309 309 merge.update(repo, p1, False, True, False)
310 310 else:
311 311 repo.ui.debug(" already in target\n")
312 312 repo.dirstate.write()
313 313 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
314 314 first = repo[rev].rev() == repo[min(state)].rev()
315 315 stats = rebasemerge(repo, rev, first)
316 316 return stats
317 317
318 318 def defineparents(repo, rev, target, state, targetancestors):
319 319 'Return the new parent relationship of the revision that will be rebased'
320 320 parents = repo[rev].parents()
321 321 p1 = p2 = nullrev
322 322
323 323 P1n = parents[0].rev()
324 324 if P1n in targetancestors:
325 325 p1 = target
326 326 elif P1n in state:
327 327 if state[P1n] == nullmerge:
328 328 p1 = target
329 329 else:
330 330 p1 = state[P1n]
331 331 else: # P1n external
332 332 p1 = target
333 333 p2 = P1n
334 334
335 335 if len(parents) == 2 and parents[1].rev() not in targetancestors:
336 336 P2n = parents[1].rev()
337 337 # interesting second parent
338 338 if P2n in state:
339 339 if p1 == target: # P1n in targetancestors or external
340 340 p1 = state[P2n]
341 341 else:
342 342 p2 = state[P2n]
343 343 else: # P2n external
344 344 if p2 != nullrev: # P1n external too => rev is a merged revision
345 345 raise util.Abort(_('cannot use revision %d as base, result '
346 346 'would have 3 parents') % rev)
347 347 p2 = P2n
348 348 repo.ui.debug(" future parents are %d and %d\n" %
349 349 (repo[p1].rev(), repo[p2].rev()))
350 350 return p1, p2
351 351
352 352 def isagitpatch(repo, patchname):
353 353 'Return true if the given patch is in git format'
354 354 mqpatch = os.path.join(repo.mq.path, patchname)
355 355 for line in patch.linereader(file(mqpatch, 'rb')):
356 356 if line.startswith('diff --git'):
357 357 return True
358 358 return False
359 359
360 360 def updatemq(repo, state, skipped, **opts):
361 361 'Update rebased mq patches - finalize and then import them'
362 362 mqrebase = {}
363 363 mq = repo.mq
364 364 original_series = mq.full_series[:]
365 365
366 366 for p in mq.applied:
367 367 rev = repo[p.node].rev()
368 368 if rev in state:
369 369 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
370 370 (rev, p.name))
371 371 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
372 372
373 373 if mqrebase:
374 374 mq.finish(repo, mqrebase.keys())
375 375
376 376 # We must start import from the newest revision
377 377 for rev in sorted(mqrebase, reverse=True):
378 378 if rev not in skipped:
379 379 name, isgit = mqrebase[rev]
380 380 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
381 381 mq.qimport(repo, (), patchname=name, git=isgit,
382 382 rev=[str(state[rev])])
383 383
384 384 # Restore missing guards
385 385 for s in original_series:
386 386 pname = mq.guard_re.split(s, 1)[0]
387 387 if pname in mq.full_series:
388 388 repo.ui.debug('restoring guard for patch %s' % (pname))
389 389 mq.full_series.remove(pname)
390 390 mq.full_series.append(s)
391 391 mq.series_dirty = True
392 392 mq.save_dirty()
393 393
394 394 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
395 395 external):
396 396 'Store the current status to allow recovery'
397 397 f = repo.opener("rebasestate", "w")
398 398 f.write(repo[originalwd].hex() + '\n')
399 399 f.write(repo[target].hex() + '\n')
400 400 f.write(repo[external].hex() + '\n')
401 401 f.write('%d\n' % int(collapse))
402 402 f.write('%d\n' % int(keep))
403 403 f.write('%d\n' % int(keepbranches))
404 404 for d, v in state.iteritems():
405 405 oldrev = repo[d].hex()
406 406 newrev = repo[v].hex()
407 407 f.write("%s:%s\n" % (oldrev, newrev))
408 408 f.close()
409 409 repo.ui.debug('rebase status stored\n')
410 410
411 411 def clearstatus(repo):
412 412 'Remove the status files'
413 413 if os.path.exists(repo.join("rebasestate")):
414 414 util.unlinkpath(repo.join("rebasestate"))
415 415
416 416 def restorestatus(repo):
417 417 'Restore a previously stored status'
418 418 try:
419 419 target = None
420 420 collapse = False
421 421 external = nullrev
422 422 state = {}
423 423 f = repo.opener("rebasestate")
424 424 for i, l in enumerate(f.read().splitlines()):
425 425 if i == 0:
426 426 originalwd = repo[l].rev()
427 427 elif i == 1:
428 428 target = repo[l].rev()
429 429 elif i == 2:
430 430 external = repo[l].rev()
431 431 elif i == 3:
432 432 collapse = bool(int(l))
433 433 elif i == 4:
434 434 keep = bool(int(l))
435 435 elif i == 5:
436 436 keepbranches = bool(int(l))
437 437 else:
438 438 oldrev, newrev = l.split(':')
439 439 state[repo[oldrev].rev()] = repo[newrev].rev()
440 440 skipped = set()
441 441 # recompute the set of skipped revs
442 442 if not collapse:
443 443 seen = set([target])
444 444 for old, new in sorted(state.items()):
445 445 if new != nullrev and new in seen:
446 446 skipped.add(old)
447 447 seen.add(new)
448 448 repo.ui.debug('computed skipped revs: %s\n' % skipped)
449 449 repo.ui.debug('rebase status resumed\n')
450 450 return (originalwd, target, state, skipped,
451 451 collapse, keep, keepbranches, external)
452 452 except IOError, err:
453 453 if err.errno != errno.ENOENT:
454 454 raise
455 455 raise util.Abort(_('no rebase in progress'))
456 456
457 457 def abort(repo, originalwd, target, state):
458 458 'Restore the repository to its original state'
459 459 if set(repo.changelog.descendants(target)) - set(state.values()):
460 460 repo.ui.warn(_("warning: new changesets detected on target branch, "
461 461 "can't abort\n"))
462 462 return -1
463 463 else:
464 464 # Strip from the first rebased revision
465 465 merge.update(repo, repo[originalwd].rev(), False, True, False)
466 466 rebased = filter(lambda x: x > -1 and x != target, state.values())
467 467 if rebased:
468 468 strippoint = min(rebased)
469 469 # no backup of rebased cset versions needed
470 470 repair.strip(repo.ui, repo, repo[strippoint].node())
471 471 clearstatus(repo)
472 472 repo.ui.warn(_('rebase aborted\n'))
473 473 return 0
474 474
475 475 def buildstate(repo, dest, src, base, detach):
476 476 'Define which revisions are going to be rebased and where'
477 477 targetancestors = set()
478 478 detachset = set()
479 479
480 480 if not dest:
481 481 # Destination defaults to the latest revision in the current branch
482 482 branch = repo[None].branch()
483 483 dest = repo[branch].rev()
484 484 else:
485 485 dest = repo[dest].rev()
486 486
487 487 # This check isn't strictly necessary, since mq detects commits over an
488 488 # applied patch. But it prevents messing up the working directory when
489 489 # a partially completed rebase is blocked by mq.
490 490 if 'qtip' in repo.tags() and (repo[dest].node() in
491 491 [s.node for s in repo.mq.applied]):
492 492 raise util.Abort(_('cannot rebase onto an applied mq patch'))
493 493
494 494 if src:
495 495 commonbase = repo[src].ancestor(repo[dest])
496 496 samebranch = repo[src].branch() == repo[dest].branch()
497 497 if commonbase == repo[src]:
498 498 raise util.Abort(_('source is ancestor of destination'))
499 499 if samebranch and commonbase == repo[dest]:
500 500 raise util.Abort(_('source is descendant of destination'))
501 501 source = repo[src].rev()
502 502 if detach:
503 503 # We need to keep track of source's ancestors up to the common base
504 504 srcancestors = set(repo.changelog.ancestors(source))
505 505 baseancestors = set(repo.changelog.ancestors(commonbase.rev()))
506 506 detachset = srcancestors - baseancestors
507 507 detachset.discard(commonbase.rev())
508 508 else:
509 509 if base:
510 510 cwd = repo[base].rev()
511 511 else:
512 512 cwd = repo['.'].rev()
513 513
514 514 if cwd == dest:
515 515 repo.ui.debug('source and destination are the same\n')
516 516 return None
517 517
518 518 targetancestors = set(repo.changelog.ancestors(dest))
519 519 if cwd in targetancestors:
520 520 repo.ui.debug('source is ancestor of destination\n')
521 521 return None
522 522
523 523 cwdancestors = set(repo.changelog.ancestors(cwd))
524 524 if dest in cwdancestors:
525 525 repo.ui.debug('source is descendant of destination\n')
526 526 return None
527 527
528 528 cwdancestors.add(cwd)
529 529 rebasingbranch = cwdancestors - targetancestors
530 530 source = min(rebasingbranch)
531 531
532 532 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
533 533 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
534 534 state.update(dict.fromkeys(detachset, nullmerge))
535 535 state[source] = nullrev
536 536 return repo['.'].rev(), repo[dest].rev(), state
537 537
538 538 def pullrebase(orig, ui, repo, *args, **opts):
539 539 'Call rebase after pull if the latter has been invoked with --rebase'
540 540 if opts.get('rebase'):
541 541 if opts.get('update'):
542 542 del opts['update']
543 543 ui.debug('--update and --rebase are not compatible, ignoring '
544 544 'the update flag\n')
545 545
546 546 cmdutil.bail_if_changed(repo)
547 547 revsprepull = len(repo)
548 548 origpostincoming = commands.postincoming
549 549 def _dummy(*args, **kwargs):
550 550 pass
551 551 commands.postincoming = _dummy
552 552 try:
553 553 orig(ui, repo, *args, **opts)
554 554 finally:
555 555 commands.postincoming = origpostincoming
556 556 revspostpull = len(repo)
557 557 if revspostpull > revsprepull:
558 558 rebase(ui, repo, **opts)
559 559 branch = repo[None].branch()
560 560 dest = repo[branch].rev()
561 561 if dest != repo['.'].rev():
562 562 # there was nothing to rebase we force an update
563 563 hg.update(repo, dest)
564 564 else:
565 565 orig(ui, repo, *args, **opts)
566 566
567 567 def uisetup(ui):
568 568 'Replace pull with a decorator to provide --rebase option'
569 569 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
570 570 entry[1].append(('', 'rebase', None,
571 571 _("rebase working directory to branch head"))
572 572 )
573 573
574 574 cmdtable = {
575 575 "rebase":
576 576 (rebase,
577 577 [
578 578 ('s', 'source', '',
579 579 _('rebase from the specified changeset'), _('REV')),
580 580 ('b', 'base', '',
581 581 _('rebase from the base of the specified changeset '
582 582 '(up to greatest common ancestor of base and dest)'),
583 583 _('REV')),
584 584 ('d', 'dest', '',
585 585 _('rebase onto the specified changeset'), _('REV')),
586 586 ('', 'collapse', False, _('collapse the rebased changesets')),
587 587 ('m', 'message', '',
588 588 _('use text as collapse commit message'), _('TEXT')),
589 589 ('l', 'logfile', '',
590 590 _('read collapse commit message from file'), _('FILE')),
591 591 ('', 'keep', False, _('keep original changesets')),
592 592 ('', 'keepbranches', False, _('keep original branch names')),
593 593 ('', 'detach', False, _('force detaching of source from its original '
594 594 'branch')),
595 595 ('c', 'continue', False, _('continue an interrupted rebase')),
596 596 ('a', 'abort', False, _('abort an interrupted rebase'))] +
597 597 templateopts,
598 598 _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
599 599 'hg rebase {-a|-c}'))
600 600 }
@@ -1,121 +1,170 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > graphlog=
4 4 > rebase=
5 5 >
6 6 > [alias]
7 7 > tlog = log --template "{rev}: '{desc}' {branches}\n"
8 8 > tglog = tlog --graph
9 9 > EOF
10 10
11 11
12 12 $ hg init a
13 13 $ cd a
14 14
15 15 $ echo a > a
16 16 $ hg ci -Am A
17 17 adding a
18 18
19 19 $ echo b > b
20 20 $ hg ci -Am B
21 21 adding b
22 22
23 23 $ hg up -q -C 0
24 24
25 25 $ hg mv a a-renamed
26 26
27 27 $ hg ci -m 'rename A'
28 28 created new head
29 29
30 30 $ hg tglog
31 31 @ 2: 'rename A'
32 32 |
33 33 | o 1: 'B'
34 34 |/
35 35 o 0: 'A'
36 36
37 37
38 38 Rename is tracked:
39 39
40 40 $ hg tlog -p --git -r tip
41 41 2: 'rename A'
42 42 diff --git a/a b/a-renamed
43 43 rename from a
44 44 rename to a-renamed
45 45
46 46 Rebase the revision containing the rename:
47 47
48 48 $ hg rebase -s 2 -d 1
49 49 saved backup bundle to $TESTTMP/a/.hg/strip-backup/*-backup.hg (glob)
50 50
51 51 $ hg tglog
52 52 @ 2: 'rename A'
53 53 |
54 54 o 1: 'B'
55 55 |
56 56 o 0: 'A'
57 57
58 58
59 59 Rename is not lost:
60 60
61 61 $ hg tlog -p --git -r tip
62 62 2: 'rename A'
63 63 diff --git a/a b/a-renamed
64 64 rename from a
65 65 rename to a-renamed
66 66
67 67 $ cd ..
68 68
69 69
70 70 $ hg init b
71 71 $ cd b
72 72
73 73 $ echo a > a
74 74 $ hg ci -Am A
75 75 adding a
76 76
77 77 $ echo b > b
78 78 $ hg ci -Am B
79 79 adding b
80 80
81 81 $ hg up -q -C 0
82 82
83 83 $ hg cp a a-copied
84 84 $ hg ci -m 'copy A'
85 85 created new head
86 86
87 87 $ hg tglog
88 88 @ 2: 'copy A'
89 89 |
90 90 | o 1: 'B'
91 91 |/
92 92 o 0: 'A'
93 93
94 94 Copy is tracked:
95 95
96 96 $ hg tlog -p --git -r tip
97 97 2: 'copy A'
98 98 diff --git a/a b/a-copied
99 99 copy from a
100 100 copy to a-copied
101 101
102 102 Rebase the revision containing the copy:
103 103
104 104 $ hg rebase -s 2 -d 1
105 105 saved backup bundle to $TESTTMP/b/.hg/strip-backup/*-backup.hg (glob)
106 106
107 107 $ hg tglog
108 108 @ 2: 'copy A'
109 109 |
110 110 o 1: 'B'
111 111 |
112 112 o 0: 'A'
113 113
114 114 Copy is not lost:
115 115
116 116 $ hg tlog -p --git -r tip
117 117 2: 'copy A'
118 118 diff --git a/a b/a-copied
119 119 copy from a
120 120 copy to a-copied
121 121
122 $ cd ..
123
124
125 Test rebase across repeating renames:
126
127 $ hg init repo
128
129 $ cd repo
130
131 $ echo testing > file1.txt
132 $ hg add file1.txt
133 $ hg ci -m "Adding file1"
134
135 $ hg rename file1.txt file2.txt
136 $ hg ci -m "Rename file1 to file2"
137
138 $ echo Unrelated change > unrelated.txt
139 $ hg add unrelated.txt
140 $ hg ci -m "Unrelated change"
141
142 $ hg rename file2.txt file1.txt
143 $ hg ci -m "Rename file2 back to file1"
144
145 $ hg update -r -2
146 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
147
148 $ echo Another unrelated change >> unrelated.txt
149 $ hg ci -m "Another unrelated change"
150 created new head
151
152 $ hg tglog
153 @ 4: 'Another unrelated change'
154 |
155 | o 3: 'Rename file2 back to file1'
156 |/
157 o 2: 'Unrelated change'
158 |
159 o 1: 'Rename file1 to file2'
160 |
161 o 0: 'Adding file1'
162
163
164 $ hg rebase -s 4 -d 3
165 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-backup.hg (glob)
166
167 $ hg diff --stat -c .
168 unrelated.txt | 1 +
169 1 files changed, 1 insertions(+), 0 deletions(-)
170
General Comments 0
You need to be logged in to leave comments. Login now