##// END OF EJS Templates
rebase: don't clobber wd on --abort when we've updated away (issue4009)
Matt Mackall -
r19516:fe78eb7b stable
parent child Browse files
Show More
@@ -1,804 +1,819
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, obsolete, error
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 revignored = -3
27 27
28 28 cmdtable = {}
29 29 command = cmdutil.command(cmdtable)
30 30 testedwith = 'internal'
31 31
32 32 @command('rebase',
33 33 [('s', 'source', '',
34 34 _('rebase from the specified changeset'), _('REV')),
35 35 ('b', 'base', '',
36 36 _('rebase from the base of the specified changeset '
37 37 '(up to greatest common ancestor of base and dest)'),
38 38 _('REV')),
39 39 ('r', 'rev', [],
40 40 _('rebase these revisions'),
41 41 _('REV')),
42 42 ('d', 'dest', '',
43 43 _('rebase onto the specified changeset'), _('REV')),
44 44 ('', 'collapse', False, _('collapse the rebased changesets')),
45 45 ('m', 'message', '',
46 46 _('use text as collapse commit message'), _('TEXT')),
47 47 ('e', 'edit', False, _('invoke editor on commit messages')),
48 48 ('l', 'logfile', '',
49 49 _('read collapse commit message from file'), _('FILE')),
50 50 ('', 'keep', False, _('keep original changesets')),
51 51 ('', 'keepbranches', False, _('keep original branch names')),
52 52 ('D', 'detach', False, _('(DEPRECATED)')),
53 53 ('t', 'tool', '', _('specify merge tool')),
54 54 ('c', 'continue', False, _('continue an interrupted rebase')),
55 55 ('a', 'abort', False, _('abort an interrupted rebase'))] +
56 56 templateopts,
57 57 _('[-s REV | -b REV] [-d REV] [OPTION]'))
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 In its default configuration, Mercurial will prevent you from
72 72 rebasing published changes. See :hg:`help phases` for details.
73 73
74 74 If you don't specify a destination changeset (``-d/--dest``),
75 75 rebase uses the current branch tip as the destination. (The
76 76 destination changeset is not modified by rebasing, but new
77 77 changesets are added as its descendants.)
78 78
79 79 You can specify which changesets to rebase in two ways: as a
80 80 "source" changeset or as a "base" changeset. Both are shorthand
81 81 for a topologically related set of changesets (the "source
82 82 branch"). If you specify source (``-s/--source``), rebase will
83 83 rebase that changeset and all of its descendants onto dest. If you
84 84 specify base (``-b/--base``), rebase will select ancestors of base
85 85 back to but not including the common ancestor with dest. Thus,
86 86 ``-b`` is less precise but more convenient than ``-s``: you can
87 87 specify any changeset in the source branch, and rebase will select
88 88 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
89 89 uses the parent of the working directory as the base.
90 90
91 91 For advanced usage, a third way is available through the ``--rev``
92 92 option. It allows you to specify an arbitrary set of changesets to
93 93 rebase. Descendants of revs you specify with this option are not
94 94 automatically included in the rebase.
95 95
96 96 By default, rebase recreates the changesets in the source branch
97 97 as descendants of dest and then destroys the originals. Use
98 98 ``--keep`` to preserve the original source changesets. Some
99 99 changesets in the source branch (e.g. merges from the destination
100 100 branch) may be dropped if they no longer contribute any change.
101 101
102 102 One result of the rules for selecting the destination changeset
103 103 and source branch is that, unlike ``merge``, rebase will do
104 104 nothing if you are at the branch tip of a named branch
105 105 with two heads. You need to explicitly specify source and/or
106 106 destination (or ``update`` to the other head, if it's the head of
107 107 the intended source branch).
108 108
109 109 If a rebase is interrupted to manually resolve a merge, it can be
110 110 continued with --continue/-c or aborted with --abort/-a.
111 111
112 112 Returns 0 on success, 1 if nothing to rebase.
113 113 """
114 114 originalwd = target = None
115 115 activebookmark = None
116 116 external = nullrev
117 117 state = {}
118 118 skipped = set()
119 119 targetancestors = set()
120 120
121 121 editor = None
122 122 if opts.get('edit'):
123 123 editor = cmdutil.commitforceeditor
124 124
125 125 lock = wlock = None
126 126 try:
127 127 wlock = repo.wlock()
128 128 lock = repo.lock()
129 129
130 130 # Validate input and define rebasing points
131 131 destf = opts.get('dest', None)
132 132 srcf = opts.get('source', None)
133 133 basef = opts.get('base', None)
134 134 revf = opts.get('rev', [])
135 135 contf = opts.get('continue')
136 136 abortf = opts.get('abort')
137 137 collapsef = opts.get('collapse', False)
138 138 collapsemsg = cmdutil.logmessage(ui, opts)
139 139 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
140 140 keepf = opts.get('keep', False)
141 141 keepbranchesf = opts.get('keepbranches', False)
142 142 # keepopen is not meant for use on the command line, but by
143 143 # other extensions
144 144 keepopen = opts.get('keepopen', False)
145 145
146 146 if collapsemsg and not collapsef:
147 147 raise util.Abort(
148 148 _('message can only be specified with collapse'))
149 149
150 150 if contf or abortf:
151 151 if contf and abortf:
152 152 raise util.Abort(_('cannot use both abort and continue'))
153 153 if collapsef:
154 154 raise util.Abort(
155 155 _('cannot use collapse with continue or abort'))
156 156 if srcf or basef or destf:
157 157 raise util.Abort(
158 158 _('abort and continue do not allow specifying revisions'))
159 159 if opts.get('tool', False):
160 160 ui.warn(_('tool option will be ignored\n'))
161 161
162 162 (originalwd, target, state, skipped, collapsef, keepf,
163 163 keepbranchesf, external, activebookmark) = restorestatus(repo)
164 164 if abortf:
165 165 return abort(repo, originalwd, target, state)
166 166 else:
167 167 if srcf and basef:
168 168 raise util.Abort(_('cannot specify both a '
169 169 'source and a base'))
170 170 if revf and basef:
171 171 raise util.Abort(_('cannot specify both a '
172 172 'revision and a base'))
173 173 if revf and srcf:
174 174 raise util.Abort(_('cannot specify both a '
175 175 'revision and a source'))
176 176
177 177 cmdutil.checkunfinished(repo)
178 178 cmdutil.bailifchanged(repo)
179 179
180 180 if not destf:
181 181 # Destination defaults to the latest revision in the
182 182 # current branch
183 183 branch = repo[None].branch()
184 184 dest = repo[branch]
185 185 else:
186 186 dest = scmutil.revsingle(repo, destf)
187 187
188 188 if revf:
189 189 rebaseset = repo.revs('%lr', revf)
190 190 elif srcf:
191 191 src = scmutil.revrange(repo, [srcf])
192 192 rebaseset = repo.revs('(%ld)::', src)
193 193 else:
194 194 base = scmutil.revrange(repo, [basef or '.'])
195 195 rebaseset = repo.revs(
196 196 '(children(ancestor(%ld, %d)) and ::(%ld))::',
197 197 base, dest, base)
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 or obsolete._enabled)
207 207 and repo.revs('first(children(%ld) - %ld)',
208 208 rebaseset, rebaseset)):
209 209 raise util.Abort(
210 210 _("can't remove original changesets with"
211 211 " unrebased descendants"),
212 212 hint=_('use --keep to keep original changesets'))
213 213 else:
214 214 result = buildstate(repo, dest, rebaseset, collapsef)
215 215
216 216 if not result:
217 217 # Empty state built, nothing to rebase
218 218 ui.status(_('nothing to rebase\n'))
219 219 return 1
220 220 elif not keepf and not repo[root].mutable():
221 221 raise util.Abort(_("can't rebase immutable changeset %s")
222 222 % repo[root],
223 223 hint=_('see hg help phases for details'))
224 224 else:
225 225 originalwd, target, state = result
226 226 if collapsef:
227 227 targetancestors = repo.changelog.ancestors([target],
228 228 inclusive=True)
229 229 external = checkexternal(repo, state, targetancestors)
230 230
231 231 if keepbranchesf:
232 232 assert not extrafn, 'cannot use both keepbranches and extrafn'
233 233 def extrafn(ctx, extra):
234 234 extra['branch'] = ctx.branch()
235 235 if collapsef:
236 236 branches = set()
237 237 for rev in state:
238 238 branches.add(repo[rev].branch())
239 239 if len(branches) > 1:
240 240 raise util.Abort(_('cannot collapse multiple named '
241 241 'branches'))
242 242
243 243
244 244 # Rebase
245 245 if not targetancestors:
246 246 targetancestors = repo.changelog.ancestors([target], inclusive=True)
247 247
248 248 # Keep track of the current bookmarks in order to reset them later
249 249 currentbookmarks = repo._bookmarks.copy()
250 250 activebookmark = activebookmark or repo._bookmarkcurrent
251 251 if activebookmark:
252 252 bookmarks.unsetcurrent(repo)
253 253
254 254 sortedstate = sorted(state)
255 255 total = len(sortedstate)
256 256 pos = 0
257 257 for rev in sortedstate:
258 258 pos += 1
259 259 if state[rev] == -1:
260 260 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
261 261 _('changesets'), total)
262 262 p1, p2 = defineparents(repo, rev, target, state,
263 263 targetancestors)
264 264 storestatus(repo, originalwd, target, state, collapsef, keepf,
265 265 keepbranchesf, external, activebookmark)
266 266 if len(repo.parents()) == 2:
267 267 repo.ui.debug('resuming interrupted rebase\n')
268 268 else:
269 269 try:
270 270 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
271 271 stats = rebasenode(repo, rev, p1, state, collapsef)
272 272 if stats and stats[3] > 0:
273 273 raise error.InterventionRequired(
274 274 _('unresolved conflicts (see hg '
275 275 'resolve, then hg rebase --continue)'))
276 276 finally:
277 277 ui.setconfig('ui', 'forcemerge', '')
278 278 cmdutil.duplicatecopies(repo, rev, target)
279 279 if not collapsef:
280 280 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
281 281 editor=editor)
282 282 else:
283 283 # Skip commit if we are collapsing
284 284 repo.setparents(repo[p1].node())
285 285 newrev = None
286 286 # Update the state
287 287 if newrev is not None:
288 288 state[rev] = repo[newrev].rev()
289 289 else:
290 290 if not collapsef:
291 291 ui.note(_('no changes, revision %d skipped\n') % rev)
292 292 ui.debug('next revision set to %s\n' % p1)
293 293 skipped.add(rev)
294 294 state[rev] = p1
295 295
296 296 ui.progress(_('rebasing'), None)
297 297 ui.note(_('rebase merging completed\n'))
298 298
299 299 if collapsef and not keepopen:
300 300 p1, p2 = defineparents(repo, min(state), target,
301 301 state, targetancestors)
302 302 if collapsemsg:
303 303 commitmsg = collapsemsg
304 304 else:
305 305 commitmsg = 'Collapsed revision'
306 306 for rebased in state:
307 307 if rebased not in skipped and state[rebased] > nullmerge:
308 308 commitmsg += '\n* %s' % repo[rebased].description()
309 309 commitmsg = ui.edit(commitmsg, repo.ui.username())
310 310 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
311 311 extrafn=extrafn, editor=editor)
312 312
313 313 if 'qtip' in repo.tags():
314 314 updatemq(repo, state, skipped, **opts)
315 315
316 316 if currentbookmarks:
317 317 # Nodeids are needed to reset bookmarks
318 318 nstate = {}
319 319 for k, v in state.iteritems():
320 320 if v > nullmerge:
321 321 nstate[repo[k].node()] = repo[v].node()
322 322 # XXX this is the same as dest.node() for the non-continue path --
323 323 # this should probably be cleaned up
324 324 targetnode = repo[target].node()
325 325
326 326 if not keepf:
327 327 collapsedas = None
328 328 if collapsef:
329 329 collapsedas = newrev
330 330 clearrebased(ui, repo, state, skipped, collapsedas)
331 331
332 332 if currentbookmarks:
333 333 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
334 334
335 335 clearstatus(repo)
336 336 ui.note(_("rebase completed\n"))
337 337 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
338 338 if skipped:
339 339 ui.note(_("%d revisions have been skipped\n") % len(skipped))
340 340
341 341 if (activebookmark and
342 342 repo['tip'].node() == repo._bookmarks[activebookmark]):
343 343 bookmarks.setcurrent(repo, activebookmark)
344 344
345 345 finally:
346 346 release(lock, wlock)
347 347
348 348 def checkexternal(repo, state, targetancestors):
349 349 """Check whether one or more external revisions need to be taken in
350 350 consideration. In the latter case, abort.
351 351 """
352 352 external = nullrev
353 353 source = min(state)
354 354 for rev in state:
355 355 if rev == source:
356 356 continue
357 357 # Check externals and fail if there are more than one
358 358 for p in repo[rev].parents():
359 359 if (p.rev() not in state
360 360 and p.rev() not in targetancestors):
361 361 if external != nullrev:
362 362 raise util.Abort(_('unable to collapse, there is more '
363 363 'than one external parent'))
364 364 external = p.rev()
365 365 return external
366 366
367 367 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
368 368 'Commit the changes and store useful information in extra'
369 369 try:
370 370 repo.setparents(repo[p1].node(), repo[p2].node())
371 371 ctx = repo[rev]
372 372 if commitmsg is None:
373 373 commitmsg = ctx.description()
374 374 extra = {'rebase_source': ctx.hex()}
375 375 if extrafn:
376 376 extrafn(ctx, extra)
377 377 # Commit might fail if unresolved files exist
378 378 newrev = repo.commit(text=commitmsg, user=ctx.user(),
379 379 date=ctx.date(), extra=extra, editor=editor)
380 380 repo.dirstate.setbranch(repo[newrev].branch())
381 381 targetphase = max(ctx.phase(), phases.draft)
382 382 # retractboundary doesn't overwrite upper phase inherited from parent
383 383 newnode = repo[newrev].node()
384 384 if newnode:
385 385 phases.retractboundary(repo, targetphase, [newnode])
386 386 return newrev
387 387 except util.Abort:
388 388 # Invalidate the previous setparents
389 389 repo.dirstate.invalidate()
390 390 raise
391 391
392 392 def rebasenode(repo, rev, p1, state, collapse):
393 393 'Rebase a single revision'
394 394 # Merge phase
395 395 # Update to target and merge it with local
396 396 if repo['.'].rev() != repo[p1].rev():
397 397 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
398 398 merge.update(repo, p1, False, True, False)
399 399 else:
400 400 repo.ui.debug(" already in target\n")
401 401 repo.dirstate.write()
402 402 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
403 403 base = None
404 404 if repo[rev].rev() != repo[min(state)].rev():
405 405 base = repo[rev].p1().node()
406 406 # When collapsing in-place, the parent is the common ancestor, we
407 407 # have to allow merging with it.
408 408 return merge.update(repo, rev, True, True, False, base, collapse)
409 409
410 410 def nearestrebased(repo, rev, state):
411 411 """return the nearest ancestors of rev in the rebase result"""
412 412 rebased = [r for r in state if state[r] > nullmerge]
413 413 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
414 414 if candidates:
415 415 return state[candidates[0]]
416 416 else:
417 417 return None
418 418
419 419 def defineparents(repo, rev, target, state, targetancestors):
420 420 'Return the new parent relationship of the revision that will be rebased'
421 421 parents = repo[rev].parents()
422 422 p1 = p2 = nullrev
423 423
424 424 P1n = parents[0].rev()
425 425 if P1n in targetancestors:
426 426 p1 = target
427 427 elif P1n in state:
428 428 if state[P1n] == nullmerge:
429 429 p1 = target
430 430 elif state[P1n] == revignored:
431 431 p1 = nearestrebased(repo, P1n, state)
432 432 if p1 is None:
433 433 p1 = target
434 434 else:
435 435 p1 = state[P1n]
436 436 else: # P1n external
437 437 p1 = target
438 438 p2 = P1n
439 439
440 440 if len(parents) == 2 and parents[1].rev() not in targetancestors:
441 441 P2n = parents[1].rev()
442 442 # interesting second parent
443 443 if P2n in state:
444 444 if p1 == target: # P1n in targetancestors or external
445 445 p1 = state[P2n]
446 446 elif state[P2n] == revignored:
447 447 p2 = nearestrebased(repo, P2n, state)
448 448 if p2 is None:
449 449 # no ancestors rebased yet, detach
450 450 p2 = target
451 451 else:
452 452 p2 = state[P2n]
453 453 else: # P2n external
454 454 if p2 != nullrev: # P1n external too => rev is a merged revision
455 455 raise util.Abort(_('cannot use revision %d as base, result '
456 456 'would have 3 parents') % rev)
457 457 p2 = P2n
458 458 repo.ui.debug(" future parents are %d and %d\n" %
459 459 (repo[p1].rev(), repo[p2].rev()))
460 460 return p1, p2
461 461
462 462 def isagitpatch(repo, patchname):
463 463 'Return true if the given patch is in git format'
464 464 mqpatch = os.path.join(repo.mq.path, patchname)
465 465 for line in patch.linereader(file(mqpatch, 'rb')):
466 466 if line.startswith('diff --git'):
467 467 return True
468 468 return False
469 469
470 470 def updatemq(repo, state, skipped, **opts):
471 471 'Update rebased mq patches - finalize and then import them'
472 472 mqrebase = {}
473 473 mq = repo.mq
474 474 original_series = mq.fullseries[:]
475 475 skippedpatches = set()
476 476
477 477 for p in mq.applied:
478 478 rev = repo[p.node].rev()
479 479 if rev in state:
480 480 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
481 481 (rev, p.name))
482 482 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
483 483 else:
484 484 # Applied but not rebased, not sure this should happen
485 485 skippedpatches.add(p.name)
486 486
487 487 if mqrebase:
488 488 mq.finish(repo, mqrebase.keys())
489 489
490 490 # We must start import from the newest revision
491 491 for rev in sorted(mqrebase, reverse=True):
492 492 if rev not in skipped:
493 493 name, isgit = mqrebase[rev]
494 494 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
495 495 mq.qimport(repo, (), patchname=name, git=isgit,
496 496 rev=[str(state[rev])])
497 497 else:
498 498 # Rebased and skipped
499 499 skippedpatches.add(mqrebase[rev][0])
500 500
501 501 # Patches were either applied and rebased and imported in
502 502 # order, applied and removed or unapplied. Discard the removed
503 503 # ones while preserving the original series order and guards.
504 504 newseries = [s for s in original_series
505 505 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
506 506 mq.fullseries[:] = newseries
507 507 mq.seriesdirty = True
508 508 mq.savedirty()
509 509
510 510 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
511 511 'Move bookmarks to their correct changesets, and delete divergent ones'
512 512 marks = repo._bookmarks
513 513 for k, v in originalbookmarks.iteritems():
514 514 if v in nstate:
515 515 # update the bookmarks for revs that have moved
516 516 marks[k] = nstate[v]
517 517 bookmarks.deletedivergent(repo, [targetnode], k)
518 518
519 519 marks.write()
520 520
521 521 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
522 522 external, activebookmark):
523 523 'Store the current status to allow recovery'
524 524 f = repo.opener("rebasestate", "w")
525 525 f.write(repo[originalwd].hex() + '\n')
526 526 f.write(repo[target].hex() + '\n')
527 527 f.write(repo[external].hex() + '\n')
528 528 f.write('%d\n' % int(collapse))
529 529 f.write('%d\n' % int(keep))
530 530 f.write('%d\n' % int(keepbranches))
531 531 f.write('%s\n' % (activebookmark or ''))
532 532 for d, v in state.iteritems():
533 533 oldrev = repo[d].hex()
534 534 if v > nullmerge:
535 535 newrev = repo[v].hex()
536 536 else:
537 537 newrev = v
538 538 f.write("%s:%s\n" % (oldrev, newrev))
539 539 f.close()
540 540 repo.ui.debug('rebase status stored\n')
541 541
542 542 def clearstatus(repo):
543 543 'Remove the status files'
544 544 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
545 545
546 546 def restorestatus(repo):
547 547 'Restore a previously stored status'
548 548 try:
549 549 target = None
550 550 collapse = False
551 551 external = nullrev
552 552 activebookmark = None
553 553 state = {}
554 554 f = repo.opener("rebasestate")
555 555 for i, l in enumerate(f.read().splitlines()):
556 556 if i == 0:
557 557 originalwd = repo[l].rev()
558 558 elif i == 1:
559 559 target = repo[l].rev()
560 560 elif i == 2:
561 561 external = repo[l].rev()
562 562 elif i == 3:
563 563 collapse = bool(int(l))
564 564 elif i == 4:
565 565 keep = bool(int(l))
566 566 elif i == 5:
567 567 keepbranches = bool(int(l))
568 568 elif i == 6 and not (len(l) == 81 and ':' in l):
569 569 # line 6 is a recent addition, so for backwards compatibility
570 570 # check that the line doesn't look like the oldrev:newrev lines
571 571 activebookmark = l
572 572 else:
573 573 oldrev, newrev = l.split(':')
574 574 if newrev in (str(nullmerge), str(revignored)):
575 575 state[repo[oldrev].rev()] = int(newrev)
576 576 else:
577 577 state[repo[oldrev].rev()] = repo[newrev].rev()
578 578 skipped = set()
579 579 # recompute the set of skipped revs
580 580 if not collapse:
581 581 seen = set([target])
582 582 for old, new in sorted(state.items()):
583 583 if new != nullrev and new in seen:
584 584 skipped.add(old)
585 585 seen.add(new)
586 586 repo.ui.debug('computed skipped revs: %s\n' % skipped)
587 587 repo.ui.debug('rebase status resumed\n')
588 588 return (originalwd, target, state, skipped,
589 589 collapse, keep, keepbranches, external, activebookmark)
590 590 except IOError, err:
591 591 if err.errno != errno.ENOENT:
592 592 raise
593 593 raise util.Abort(_('no rebase in progress'))
594 594
595 def inrebase(repo, originalwd, state):
596 '''check whether the workdir is in an interrupted rebase'''
597 parents = [p.rev() for p in repo.parents()]
598 if originalwd in parents:
599 return True
600
601 for newrev in state.itervalues():
602 if newrev in parents:
603 return True
604
605 return False
606
595 607 def abort(repo, originalwd, target, state):
596 608 'Restore the repository to its original state'
597 609 dstates = [s for s in state.values() if s != nullrev]
598 610 immutable = [d for d in dstates if not repo[d].mutable()]
599 611 if immutable:
600 612 raise util.Abort(_("can't abort rebase due to immutable changesets %s")
601 613 % ', '.join(str(repo[r]) for r in immutable),
602 614 hint=_('see hg help phases for details'))
603 615
604 616 descendants = set()
605 617 if dstates:
606 618 descendants = set(repo.changelog.descendants(dstates))
607 619 if descendants - set(dstates):
608 620 repo.ui.warn(_("warning: new changesets detected on target branch, "
609 621 "can't abort\n"))
610 622 return -1
611 623 else:
624 # Update away from the rebase if necessary
625 if inrebase(repo, originalwd, state):
626 merge.update(repo, repo[originalwd].rev(), False, True, False)
627
612 628 # Strip from the first rebased revision
613 merge.update(repo, repo[originalwd].rev(), False, True, False)
614 629 rebased = filter(lambda x: x > -1 and x != target, state.values())
615 630 if rebased:
616 631 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
617 632 # no backup of rebased cset versions needed
618 633 repair.strip(repo.ui, repo, strippoints)
619 634 clearstatus(repo)
620 635 repo.ui.warn(_('rebase aborted\n'))
621 636 return 0
622 637
623 638 def buildstate(repo, dest, rebaseset, collapse):
624 639 '''Define which revisions are going to be rebased and where
625 640
626 641 repo: repo
627 642 dest: context
628 643 rebaseset: set of rev
629 644 '''
630 645
631 646 # This check isn't strictly necessary, since mq detects commits over an
632 647 # applied patch. But it prevents messing up the working directory when
633 648 # a partially completed rebase is blocked by mq.
634 649 if 'qtip' in repo.tags() and (dest.node() in
635 650 [s.node for s in repo.mq.applied]):
636 651 raise util.Abort(_('cannot rebase onto an applied mq patch'))
637 652
638 653 roots = list(repo.set('roots(%ld)', rebaseset))
639 654 if not roots:
640 655 raise util.Abort(_('no matching revisions'))
641 656 roots.sort()
642 657 state = {}
643 658 detachset = set()
644 659 for root in roots:
645 660 commonbase = root.ancestor(dest)
646 661 if commonbase == root:
647 662 raise util.Abort(_('source is ancestor of destination'))
648 663 if commonbase == dest:
649 664 samebranch = root.branch() == dest.branch()
650 665 if not collapse and samebranch and root in dest.children():
651 666 repo.ui.debug('source is a child of destination\n')
652 667 return None
653 668
654 669 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))
655 670 state.update(dict.fromkeys(rebaseset, nullrev))
656 671 # Rebase tries to turn <dest> into a parent of <root> while
657 672 # preserving the number of parents of rebased changesets:
658 673 #
659 674 # - A changeset with a single parent will always be rebased as a
660 675 # changeset with a single parent.
661 676 #
662 677 # - A merge will be rebased as merge unless its parents are both
663 678 # ancestors of <dest> or are themselves in the rebased set and
664 679 # pruned while rebased.
665 680 #
666 681 # If one parent of <root> is an ancestor of <dest>, the rebased
667 682 # version of this parent will be <dest>. This is always true with
668 683 # --base option.
669 684 #
670 685 # Otherwise, we need to *replace* the original parents with
671 686 # <dest>. This "detaches" the rebased set from its former location
672 687 # and rebases it onto <dest>. Changes introduced by ancestors of
673 688 # <root> not common with <dest> (the detachset, marked as
674 689 # nullmerge) are "removed" from the rebased changesets.
675 690 #
676 691 # - If <root> has a single parent, set it to <dest>.
677 692 #
678 693 # - If <root> is a merge, we cannot decide which parent to
679 694 # replace, the rebase operation is not clearly defined.
680 695 #
681 696 # The table below sums up this behavior:
682 697 #
683 698 # +------------------+----------------------+-------------------------+
684 699 # | | one parent | merge |
685 700 # +------------------+----------------------+-------------------------+
686 701 # | parent in | new parent is <dest> | parents in ::<dest> are |
687 702 # | ::<dest> | | remapped to <dest> |
688 703 # +------------------+----------------------+-------------------------+
689 704 # | unrelated source | new parent is <dest> | ambiguous, abort |
690 705 # +------------------+----------------------+-------------------------+
691 706 #
692 707 # The actual abort is handled by `defineparents`
693 708 if len(root.parents()) <= 1:
694 709 # ancestors of <root> not ancestors of <dest>
695 710 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
696 711 [root.rev()]))
697 712 for r in detachset:
698 713 if r not in state:
699 714 state[r] = nullmerge
700 715 if len(roots) > 1:
701 716 # If we have multiple roots, we may have "hole" in the rebase set.
702 717 # Rebase roots that descend from those "hole" should not be detached as
703 718 # other root are. We use the special `revignored` to inform rebase that
704 719 # the revision should be ignored but that `defineparents` should search
705 720 # a rebase destination that make sense regarding rebased topology.
706 721 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
707 722 for ignored in set(rebasedomain) - set(rebaseset):
708 723 state[ignored] = revignored
709 724 return repo['.'].rev(), dest.rev(), state
710 725
711 726 def clearrebased(ui, repo, state, skipped, collapsedas=None):
712 727 """dispose of rebased revision at the end of the rebase
713 728
714 729 If `collapsedas` is not None, the rebase was a collapse whose result if the
715 730 `collapsedas` node."""
716 731 if obsolete._enabled:
717 732 markers = []
718 733 for rev, newrev in sorted(state.items()):
719 734 if newrev >= 0:
720 735 if rev in skipped:
721 736 succs = ()
722 737 elif collapsedas is not None:
723 738 succs = (repo[collapsedas],)
724 739 else:
725 740 succs = (repo[newrev],)
726 741 markers.append((repo[rev], succs))
727 742 if markers:
728 743 obsolete.createmarkers(repo, markers)
729 744 else:
730 745 rebased = [rev for rev in state if state[rev] > nullmerge]
731 746 if rebased:
732 747 stripped = []
733 748 for root in repo.set('roots(%ld)', rebased):
734 749 if set(repo.changelog.descendants([root.rev()])) - set(state):
735 750 ui.warn(_("warning: new changesets detected "
736 751 "on source branch, not stripping\n"))
737 752 else:
738 753 stripped.append(root.node())
739 754 if stripped:
740 755 # backup the old csets by default
741 756 repair.strip(ui, repo, stripped, "all")
742 757
743 758
744 759 def pullrebase(orig, ui, repo, *args, **opts):
745 760 'Call rebase after pull if the latter has been invoked with --rebase'
746 761 if opts.get('rebase'):
747 762 if opts.get('update'):
748 763 del opts['update']
749 764 ui.debug('--update and --rebase are not compatible, ignoring '
750 765 'the update flag\n')
751 766
752 767 movemarkfrom = repo['.'].node()
753 768 cmdutil.bailifchanged(repo)
754 769 revsprepull = len(repo)
755 770 origpostincoming = commands.postincoming
756 771 def _dummy(*args, **kwargs):
757 772 pass
758 773 commands.postincoming = _dummy
759 774 try:
760 775 orig(ui, repo, *args, **opts)
761 776 finally:
762 777 commands.postincoming = origpostincoming
763 778 revspostpull = len(repo)
764 779 if revspostpull > revsprepull:
765 780 # --rev option from pull conflict with rebase own --rev
766 781 # dropping it
767 782 if 'rev' in opts:
768 783 del opts['rev']
769 784 rebase(ui, repo, **opts)
770 785 branch = repo[None].branch()
771 786 dest = repo[branch].rev()
772 787 if dest != repo['.'].rev():
773 788 # there was nothing to rebase we force an update
774 789 hg.update(repo, dest)
775 790 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
776 791 ui.status(_("updating bookmark %s\n")
777 792 % repo._bookmarkcurrent)
778 793 else:
779 794 if opts.get('tool'):
780 795 raise util.Abort(_('--tool can only be used with --rebase'))
781 796 orig(ui, repo, *args, **opts)
782 797
783 798 def summaryhook(ui, repo):
784 799 if not os.path.exists(repo.join('rebasestate')):
785 800 return
786 801 state = restorestatus(repo)[2]
787 802 numrebased = len([i for i in state.itervalues() if i != -1])
788 803 # i18n: column positioning for "hg summary"
789 804 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
790 805 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
791 806 ui.label(_('%d remaining'), 'rebase.remaining') %
792 807 (len(state) - numrebased)))
793 808
794 809 def uisetup(ui):
795 810 'Replace pull with a decorator to provide --rebase option'
796 811 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
797 812 entry[1].append(('', 'rebase', None,
798 813 _("rebase working directory to branch head")))
799 814 entry[1].append(('t', 'tool', '',
800 815 _("specify merge tool for rebase")))
801 816 cmdutil.summaryhooks.add('rebase', summaryhook)
802 817 cmdutil.unfinishedstates.append(
803 818 ['rebasestate', False, False, _('rebase in progress'),
804 819 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now