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