##// END OF EJS Templates
rebase: properly calculate descendant set when aborting (issue3332)...
Matt Mackall -
r16280:08068233 stable
parent child Browse files
Show More
@@ -1,664 +1,667 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 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
450 450 for p in mq.applied:
451 451 rev = repo[p.node].rev()
452 452 if rev in state:
453 453 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
454 454 (rev, p.name))
455 455 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
456 456
457 457 if mqrebase:
458 458 mq.finish(repo, mqrebase.keys())
459 459
460 460 # We must start import from the newest revision
461 461 for rev in sorted(mqrebase, reverse=True):
462 462 if rev not in skipped:
463 463 name, isgit = mqrebase[rev]
464 464 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
465 465 mq.qimport(repo, (), patchname=name, git=isgit,
466 466 rev=[str(state[rev])])
467 467
468 468 # restore missing guards
469 469 for s in original_series:
470 470 pname = mq.guard_re.split(s, 1)[0]
471 471 if pname in mq.fullseries:
472 472 repo.ui.debug('restoring guard for patch %s' % (pname))
473 473 mq.fullseries[mq.fullseries.index(pname)] = s
474 474 mq.series_dirty = True
475 475 mq.savedirty()
476 476
477 477 def updatebookmarks(repo, nstate, originalbookmarks, **opts):
478 478 'Move bookmarks to their correct changesets'
479 479 current = repo._bookmarkcurrent
480 480 for k, v in originalbookmarks.iteritems():
481 481 if v in nstate:
482 482 if nstate[v] != nullmerge:
483 483 # reset the pointer if the bookmark was moved incorrectly
484 484 if k != current:
485 485 repo._bookmarks[k] = nstate[v]
486 486
487 487 bookmarks.write(repo)
488 488
489 489 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
490 490 external):
491 491 'Store the current status to allow recovery'
492 492 f = repo.opener("rebasestate", "w")
493 493 f.write(repo[originalwd].hex() + '\n')
494 494 f.write(repo[target].hex() + '\n')
495 495 f.write(repo[external].hex() + '\n')
496 496 f.write('%d\n' % int(collapse))
497 497 f.write('%d\n' % int(keep))
498 498 f.write('%d\n' % int(keepbranches))
499 499 for d, v in state.iteritems():
500 500 oldrev = repo[d].hex()
501 501 if v != nullmerge:
502 502 newrev = repo[v].hex()
503 503 else:
504 504 newrev = v
505 505 f.write("%s:%s\n" % (oldrev, newrev))
506 506 f.close()
507 507 repo.ui.debug('rebase status stored\n')
508 508
509 509 def clearstatus(repo):
510 510 'Remove the status files'
511 511 if os.path.exists(repo.join("rebasestate")):
512 512 util.unlinkpath(repo.join("rebasestate"))
513 513
514 514 def restorestatus(repo):
515 515 'Restore a previously stored status'
516 516 try:
517 517 target = None
518 518 collapse = False
519 519 external = nullrev
520 520 state = {}
521 521 f = repo.opener("rebasestate")
522 522 for i, l in enumerate(f.read().splitlines()):
523 523 if i == 0:
524 524 originalwd = repo[l].rev()
525 525 elif i == 1:
526 526 target = repo[l].rev()
527 527 elif i == 2:
528 528 external = repo[l].rev()
529 529 elif i == 3:
530 530 collapse = bool(int(l))
531 531 elif i == 4:
532 532 keep = bool(int(l))
533 533 elif i == 5:
534 534 keepbranches = bool(int(l))
535 535 else:
536 536 oldrev, newrev = l.split(':')
537 537 if newrev != str(nullmerge):
538 538 state[repo[oldrev].rev()] = repo[newrev].rev()
539 539 else:
540 540 state[repo[oldrev].rev()] = int(newrev)
541 541 skipped = set()
542 542 # recompute the set of skipped revs
543 543 if not collapse:
544 544 seen = set([target])
545 545 for old, new in sorted(state.items()):
546 546 if new != nullrev and new in seen:
547 547 skipped.add(old)
548 548 seen.add(new)
549 549 repo.ui.debug('computed skipped revs: %s\n' % skipped)
550 550 repo.ui.debug('rebase status resumed\n')
551 551 return (originalwd, target, state, skipped,
552 552 collapse, keep, keepbranches, external)
553 553 except IOError, err:
554 554 if err.errno != errno.ENOENT:
555 555 raise
556 556 raise util.Abort(_('no rebase in progress'))
557 557
558 558 def abort(repo, originalwd, target, state):
559 559 'Restore the repository to its original state'
560 descendants = repo.changelog.descendants
561 ispublic = lambda r: repo._phaserev[r] == phases.public
562 if filter(ispublic, descendants(target)):
560 dstates = [s for s in state.values() if s != nullrev]
561 if [d for d in dstates if not repo[d].mutable()]:
563 562 repo.ui.warn(_("warning: immutable rebased changeset detected, "
564 563 "can't abort\n"))
565 564 return -1
566 elif set(descendants(target)) - set(state.values()):
565
566 descendants = set()
567 if dstates:
568 descendants = set(repo.changelog.descendants(*dstates))
569 if descendants - set(dstates):
567 570 repo.ui.warn(_("warning: new changesets detected on target branch, "
568 571 "can't abort\n"))
569 572 return -1
570 573 else:
571 574 # Strip from the first rebased revision
572 575 merge.update(repo, repo[originalwd].rev(), False, True, False)
573 576 rebased = filter(lambda x: x > -1 and x != target, state.values())
574 577 if rebased:
575 578 strippoint = min(rebased)
576 579 # no backup of rebased cset versions needed
577 580 repair.strip(repo.ui, repo, repo[strippoint].node())
578 581 clearstatus(repo)
579 582 repo.ui.warn(_('rebase aborted\n'))
580 583 return 0
581 584
582 585 def buildstate(repo, dest, rebaseset, detach):
583 586 '''Define which revisions are going to be rebased and where
584 587
585 588 repo: repo
586 589 dest: context
587 590 rebaseset: set of rev
588 591 detach: boolean'''
589 592
590 593 # This check isn't strictly necessary, since mq detects commits over an
591 594 # applied patch. But it prevents messing up the working directory when
592 595 # a partially completed rebase is blocked by mq.
593 596 if 'qtip' in repo.tags() and (dest.node() in
594 597 [s.node for s in repo.mq.applied]):
595 598 raise util.Abort(_('cannot rebase onto an applied mq patch'))
596 599
597 600 detachset = set()
598 601 roots = list(repo.set('roots(%ld)', rebaseset))
599 602 if not roots:
600 603 raise util.Abort(_('no matching revisions'))
601 604 if len(roots) > 1:
602 605 raise util.Abort(_("can't rebase multiple roots"))
603 606 root = roots[0]
604 607
605 608 commonbase = root.ancestor(dest)
606 609 if commonbase == root:
607 610 raise util.Abort(_('source is ancestor of destination'))
608 611 if commonbase == dest:
609 612 samebranch = root.branch() == dest.branch()
610 613 if samebranch and root in dest.children():
611 614 repo.ui.debug('source is a child of destination')
612 615 return None
613 616 # rebase on ancestor, force detach
614 617 detach = True
615 618 if detach:
616 619 detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root)
617 620
618 621 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
619 622 state = dict.fromkeys(rebaseset, nullrev)
620 623 state.update(dict.fromkeys(detachset, nullmerge))
621 624 return repo['.'].rev(), dest.rev(), state
622 625
623 626 def pullrebase(orig, ui, repo, *args, **opts):
624 627 'Call rebase after pull if the latter has been invoked with --rebase'
625 628 if opts.get('rebase'):
626 629 if opts.get('update'):
627 630 del opts['update']
628 631 ui.debug('--update and --rebase are not compatible, ignoring '
629 632 'the update flag\n')
630 633
631 634 movemarkfrom = repo['.'].node()
632 635 cmdutil.bailifchanged(repo)
633 636 revsprepull = len(repo)
634 637 origpostincoming = commands.postincoming
635 638 def _dummy(*args, **kwargs):
636 639 pass
637 640 commands.postincoming = _dummy
638 641 try:
639 642 orig(ui, repo, *args, **opts)
640 643 finally:
641 644 commands.postincoming = origpostincoming
642 645 revspostpull = len(repo)
643 646 if revspostpull > revsprepull:
644 647 rebase(ui, repo, **opts)
645 648 branch = repo[None].branch()
646 649 dest = repo[branch].rev()
647 650 if dest != repo['.'].rev():
648 651 # there was nothing to rebase we force an update
649 652 hg.update(repo, dest)
650 653 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
651 654 ui.status(_("updating bookmark %s\n")
652 655 % repo._bookmarkcurrent)
653 656 else:
654 657 if opts.get('tool'):
655 658 raise util.Abort(_('--tool can only be used with --rebase'))
656 659 orig(ui, repo, *args, **opts)
657 660
658 661 def uisetup(ui):
659 662 'Replace pull with a decorator to provide --rebase option'
660 663 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
661 664 entry[1].append(('', 'rebase', None,
662 665 _("rebase working directory to branch head")))
663 666 entry[1].append(('t', 'tool', '',
664 667 _("specify merge tool for rebase")))
General Comments 0
You need to be logged in to leave comments. Login now