##// END OF EJS Templates
rebase: only advance phase on successful commit
Matt Mackall -
r15923:4b088ae9 default
parent child Browse files
Show More
@@ -1,659 +1,660 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 185 dest = 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')
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.dirstate.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.dirstate.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 phases.retractboundary(repo, targetphase, [newnode])
378 if newnode:
379 phases.retractboundary(repo, targetphase, [newnode])
379 380 return newrev
380 381 except util.Abort:
381 382 # Invalidate the previous setparents
382 383 repo.dirstate.invalidate()
383 384 raise
384 385
385 386 def rebasenode(repo, rev, p1, state):
386 387 'Rebase a single revision'
387 388 # Merge phase
388 389 # Update to target and merge it with local
389 390 if repo['.'].rev() != repo[p1].rev():
390 391 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
391 392 merge.update(repo, p1, False, True, False)
392 393 else:
393 394 repo.ui.debug(" already in target\n")
394 395 repo.dirstate.write()
395 396 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
396 397 base = None
397 398 if repo[rev].rev() != repo[min(state)].rev():
398 399 base = repo[rev].p1().node()
399 400 return merge.update(repo, rev, True, True, False, base)
400 401
401 402 def defineparents(repo, rev, target, state, targetancestors):
402 403 'Return the new parent relationship of the revision that will be rebased'
403 404 parents = repo[rev].parents()
404 405 p1 = p2 = nullrev
405 406
406 407 P1n = parents[0].rev()
407 408 if P1n in targetancestors:
408 409 p1 = target
409 410 elif P1n in state:
410 411 if state[P1n] == nullmerge:
411 412 p1 = target
412 413 else:
413 414 p1 = state[P1n]
414 415 else: # P1n external
415 416 p1 = target
416 417 p2 = P1n
417 418
418 419 if len(parents) == 2 and parents[1].rev() not in targetancestors:
419 420 P2n = parents[1].rev()
420 421 # interesting second parent
421 422 if P2n in state:
422 423 if p1 == target: # P1n in targetancestors or external
423 424 p1 = state[P2n]
424 425 else:
425 426 p2 = state[P2n]
426 427 else: # P2n external
427 428 if p2 != nullrev: # P1n external too => rev is a merged revision
428 429 raise util.Abort(_('cannot use revision %d as base, result '
429 430 'would have 3 parents') % rev)
430 431 p2 = P2n
431 432 repo.ui.debug(" future parents are %d and %d\n" %
432 433 (repo[p1].rev(), repo[p2].rev()))
433 434 return p1, p2
434 435
435 436 def isagitpatch(repo, patchname):
436 437 'Return true if the given patch is in git format'
437 438 mqpatch = os.path.join(repo.mq.path, patchname)
438 439 for line in patch.linereader(file(mqpatch, 'rb')):
439 440 if line.startswith('diff --git'):
440 441 return True
441 442 return False
442 443
443 444 def updatemq(repo, state, skipped, **opts):
444 445 'Update rebased mq patches - finalize and then import them'
445 446 mqrebase = {}
446 447 mq = repo.mq
447 448 original_series = mq.fullseries[:]
448 449
449 450 for p in mq.applied:
450 451 rev = repo[p.node].rev()
451 452 if rev in state:
452 453 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
453 454 (rev, p.name))
454 455 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
455 456
456 457 if mqrebase:
457 458 mq.finish(repo, mqrebase.keys())
458 459
459 460 # We must start import from the newest revision
460 461 for rev in sorted(mqrebase, reverse=True):
461 462 if rev not in skipped:
462 463 name, isgit = mqrebase[rev]
463 464 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
464 465 mq.qimport(repo, (), patchname=name, git=isgit,
465 466 rev=[str(state[rev])])
466 467
467 468 # restore missing guards
468 469 for s in original_series:
469 470 pname = mq.guard_re.split(s, 1)[0]
470 471 if pname in mq.fullseries:
471 472 repo.ui.debug('restoring guard for patch %s' % (pname))
472 473 mq.fullseries[mq.fullseries.index(pname)] = s
473 474 mq.series_dirty = True
474 475 mq.savedirty()
475 476
476 477 def updatebookmarks(repo, nstate, originalbookmarks, **opts):
477 478 'Move bookmarks to their correct changesets'
478 479 current = repo._bookmarkcurrent
479 480 for k, v in originalbookmarks.iteritems():
480 481 if v in nstate:
481 482 if nstate[v] != nullmerge:
482 483 # reset the pointer if the bookmark was moved incorrectly
483 484 if k != current:
484 485 repo._bookmarks[k] = nstate[v]
485 486
486 487 bookmarks.write(repo)
487 488
488 489 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
489 490 external):
490 491 'Store the current status to allow recovery'
491 492 f = repo.opener("rebasestate", "w")
492 493 f.write(repo[originalwd].hex() + '\n')
493 494 f.write(repo[target].hex() + '\n')
494 495 f.write(repo[external].hex() + '\n')
495 496 f.write('%d\n' % int(collapse))
496 497 f.write('%d\n' % int(keep))
497 498 f.write('%d\n' % int(keepbranches))
498 499 for d, v in state.iteritems():
499 500 oldrev = repo[d].hex()
500 501 if v != nullmerge:
501 502 newrev = repo[v].hex()
502 503 else:
503 504 newrev = v
504 505 f.write("%s:%s\n" % (oldrev, newrev))
505 506 f.close()
506 507 repo.ui.debug('rebase status stored\n')
507 508
508 509 def clearstatus(repo):
509 510 'Remove the status files'
510 511 if os.path.exists(repo.join("rebasestate")):
511 512 util.unlinkpath(repo.join("rebasestate"))
512 513
513 514 def restorestatus(repo):
514 515 'Restore a previously stored status'
515 516 try:
516 517 target = None
517 518 collapse = False
518 519 external = nullrev
519 520 state = {}
520 521 f = repo.opener("rebasestate")
521 522 for i, l in enumerate(f.read().splitlines()):
522 523 if i == 0:
523 524 originalwd = repo[l].rev()
524 525 elif i == 1:
525 526 target = repo[l].rev()
526 527 elif i == 2:
527 528 external = repo[l].rev()
528 529 elif i == 3:
529 530 collapse = bool(int(l))
530 531 elif i == 4:
531 532 keep = bool(int(l))
532 533 elif i == 5:
533 534 keepbranches = bool(int(l))
534 535 else:
535 536 oldrev, newrev = l.split(':')
536 537 if newrev != str(nullmerge):
537 538 state[repo[oldrev].rev()] = repo[newrev].rev()
538 539 else:
539 540 state[repo[oldrev].rev()] = int(newrev)
540 541 skipped = set()
541 542 # recompute the set of skipped revs
542 543 if not collapse:
543 544 seen = set([target])
544 545 for old, new in sorted(state.items()):
545 546 if new != nullrev and new in seen:
546 547 skipped.add(old)
547 548 seen.add(new)
548 549 repo.ui.debug('computed skipped revs: %s\n' % skipped)
549 550 repo.ui.debug('rebase status resumed\n')
550 551 return (originalwd, target, state, skipped,
551 552 collapse, keep, keepbranches, external)
552 553 except IOError, err:
553 554 if err.errno != errno.ENOENT:
554 555 raise
555 556 raise util.Abort(_('no rebase in progress'))
556 557
557 558 def abort(repo, originalwd, target, state):
558 559 'Restore the repository to its original state'
559 560 descendants = repo.changelog.descendants
560 561 ispublic = lambda r: repo._phaserev[r] == phases.public
561 562 if filter(ispublic, descendants(target)):
562 563 repo.ui.warn(_("warning: immutable rebased changeset detected, "
563 564 "can't abort\n"))
564 565 return -1
565 566 elif set(descendants(target)) - set(state.values()):
566 567 repo.ui.warn(_("warning: new changesets detected on target branch, "
567 568 "can't abort\n"))
568 569 return -1
569 570 else:
570 571 # Strip from the first rebased revision
571 572 merge.update(repo, repo[originalwd].rev(), False, True, False)
572 573 rebased = filter(lambda x: x > -1 and x != target, state.values())
573 574 if rebased:
574 575 strippoint = min(rebased)
575 576 # no backup of rebased cset versions needed
576 577 repair.strip(repo.ui, repo, repo[strippoint].node())
577 578 clearstatus(repo)
578 579 repo.ui.warn(_('rebase aborted\n'))
579 580 return 0
580 581
581 582 def buildstate(repo, dest, rebaseset, detach):
582 583 '''Define which revisions are going to be rebased and where
583 584
584 585 repo: repo
585 586 dest: context
586 587 rebaseset: set of rev
587 588 detach: boolean'''
588 589
589 590 # This check isn't strictly necessary, since mq detects commits over an
590 591 # applied patch. But it prevents messing up the working directory when
591 592 # a partially completed rebase is blocked by mq.
592 593 if 'qtip' in repo.tags() and (dest.node() in
593 594 [s.node for s in repo.mq.applied]):
594 595 raise util.Abort(_('cannot rebase onto an applied mq patch'))
595 596
596 597 detachset = set()
597 598 roots = list(repo.set('roots(%ld)', rebaseset))
598 599 if not roots:
599 600 raise util.Abort(_('no matching revisions'))
600 601 if len(roots) > 1:
601 602 raise util.Abort(_("can't rebase multiple roots"))
602 603 root = roots[0]
603 604
604 605 commonbase = root.ancestor(dest)
605 606 if commonbase == root:
606 607 raise util.Abort(_('source is ancestor of destination'))
607 608 if commonbase == dest:
608 609 samebranch = root.branch() == dest.branch()
609 610 if samebranch and root in dest.children():
610 611 repo.ui.debug('source is a child of destination')
611 612 return None
612 613 # rebase on ancestor, force detach
613 614 detach = True
614 615 if detach:
615 616 detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root)
616 617
617 618 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
618 619 state = dict.fromkeys(rebaseset, nullrev)
619 620 state.update(dict.fromkeys(detachset, nullmerge))
620 621 return repo['.'].rev(), dest.rev(), state
621 622
622 623 def pullrebase(orig, ui, repo, *args, **opts):
623 624 'Call rebase after pull if the latter has been invoked with --rebase'
624 625 if opts.get('rebase'):
625 626 if opts.get('update'):
626 627 del opts['update']
627 628 ui.debug('--update and --rebase are not compatible, ignoring '
628 629 'the update flag\n')
629 630
630 631 cmdutil.bailifchanged(repo)
631 632 revsprepull = len(repo)
632 633 origpostincoming = commands.postincoming
633 634 def _dummy(*args, **kwargs):
634 635 pass
635 636 commands.postincoming = _dummy
636 637 try:
637 638 orig(ui, repo, *args, **opts)
638 639 finally:
639 640 commands.postincoming = origpostincoming
640 641 revspostpull = len(repo)
641 642 if revspostpull > revsprepull:
642 643 rebase(ui, repo, **opts)
643 644 branch = repo[None].branch()
644 645 dest = repo[branch].rev()
645 646 if dest != repo['.'].rev():
646 647 # there was nothing to rebase we force an update
647 648 hg.update(repo, dest)
648 649 else:
649 650 if opts.get('tool'):
650 651 raise util.Abort(_('--tool can only be used with --rebase'))
651 652 orig(ui, repo, *args, **opts)
652 653
653 654 def uisetup(ui):
654 655 'Replace pull with a decorator to provide --rebase option'
655 656 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
656 657 entry[1].append(('', 'rebase', None,
657 658 _("rebase working directory to branch head")))
658 659 entry[1].append(('t', 'tool', '',
659 660 _("specify merge tool for rebase")))
General Comments 0
You need to be logged in to leave comments. Login now