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