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