##// END OF EJS Templates
rebase: continue abort without strip for immutable csets (issue3997)...
Matt Mackall -
r19517:eab2ff59 stable
parent child Browse files
Show More
@@ -1,819 +1,819 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, 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 595 def inrebase(repo, originalwd, state):
596 596 '''check whether the workdir is in an interrupted rebase'''
597 597 parents = [p.rev() for p in repo.parents()]
598 598 if originalwd in parents:
599 599 return True
600 600
601 601 for newrev in state.itervalues():
602 602 if newrev in parents:
603 603 return True
604 604
605 605 return False
606 606
607 607 def abort(repo, originalwd, target, state):
608 608 'Restore the repository to its original state'
609 609 dstates = [s for s in state.values() if s != nullrev]
610 610 immutable = [d for d in dstates if not repo[d].mutable()]
611 611 if immutable:
612 raise util.Abort(_("can't abort rebase due to immutable changesets %s")
613 % ', '.join(str(repo[r]) for r in immutable),
614 hint=_('see hg help phases for details'))
612 repo.ui.warn(_("warning: can't clean up immutable changesets %s\n")
613 % ', '.join(str(repo[r]) for r in immutable),
614 hint=_('see hg help phases for details'))
615 615
616 616 descendants = set()
617 617 if dstates:
618 618 descendants = set(repo.changelog.descendants(dstates))
619 619 if descendants - set(dstates):
620 620 repo.ui.warn(_("warning: new changesets detected on target branch, "
621 621 "can't abort\n"))
622 622 return -1
623 623 else:
624 624 # Update away from the rebase if necessary
625 if inrebase(repo, originalwd, state):
625 if not immutable and inrebase(repo, originalwd, state):
626 626 merge.update(repo, repo[originalwd].rev(), False, True, False)
627 627
628 628 # Strip from the first rebased revision
629 629 rebased = filter(lambda x: x > -1 and x != target, state.values())
630 if rebased:
630 if rebased and not immutable:
631 631 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
632 632 # no backup of rebased cset versions needed
633 633 repair.strip(repo.ui, repo, strippoints)
634 634 clearstatus(repo)
635 635 repo.ui.warn(_('rebase aborted\n'))
636 636 return 0
637 637
638 638 def buildstate(repo, dest, rebaseset, collapse):
639 639 '''Define which revisions are going to be rebased and where
640 640
641 641 repo: repo
642 642 dest: context
643 643 rebaseset: set of rev
644 644 '''
645 645
646 646 # This check isn't strictly necessary, since mq detects commits over an
647 647 # applied patch. But it prevents messing up the working directory when
648 648 # a partially completed rebase is blocked by mq.
649 649 if 'qtip' in repo.tags() and (dest.node() in
650 650 [s.node for s in repo.mq.applied]):
651 651 raise util.Abort(_('cannot rebase onto an applied mq patch'))
652 652
653 653 roots = list(repo.set('roots(%ld)', rebaseset))
654 654 if not roots:
655 655 raise util.Abort(_('no matching revisions'))
656 656 roots.sort()
657 657 state = {}
658 658 detachset = set()
659 659 for root in roots:
660 660 commonbase = root.ancestor(dest)
661 661 if commonbase == root:
662 662 raise util.Abort(_('source is ancestor of destination'))
663 663 if commonbase == dest:
664 664 samebranch = root.branch() == dest.branch()
665 665 if not collapse and samebranch and root in dest.children():
666 666 repo.ui.debug('source is a child of destination\n')
667 667 return None
668 668
669 669 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))
670 670 state.update(dict.fromkeys(rebaseset, nullrev))
671 671 # Rebase tries to turn <dest> into a parent of <root> while
672 672 # preserving the number of parents of rebased changesets:
673 673 #
674 674 # - A changeset with a single parent will always be rebased as a
675 675 # changeset with a single parent.
676 676 #
677 677 # - A merge will be rebased as merge unless its parents are both
678 678 # ancestors of <dest> or are themselves in the rebased set and
679 679 # pruned while rebased.
680 680 #
681 681 # If one parent of <root> is an ancestor of <dest>, the rebased
682 682 # version of this parent will be <dest>. This is always true with
683 683 # --base option.
684 684 #
685 685 # Otherwise, we need to *replace* the original parents with
686 686 # <dest>. This "detaches" the rebased set from its former location
687 687 # and rebases it onto <dest>. Changes introduced by ancestors of
688 688 # <root> not common with <dest> (the detachset, marked as
689 689 # nullmerge) are "removed" from the rebased changesets.
690 690 #
691 691 # - If <root> has a single parent, set it to <dest>.
692 692 #
693 693 # - If <root> is a merge, we cannot decide which parent to
694 694 # replace, the rebase operation is not clearly defined.
695 695 #
696 696 # The table below sums up this behavior:
697 697 #
698 698 # +------------------+----------------------+-------------------------+
699 699 # | | one parent | merge |
700 700 # +------------------+----------------------+-------------------------+
701 701 # | parent in | new parent is <dest> | parents in ::<dest> are |
702 702 # | ::<dest> | | remapped to <dest> |
703 703 # +------------------+----------------------+-------------------------+
704 704 # | unrelated source | new parent is <dest> | ambiguous, abort |
705 705 # +------------------+----------------------+-------------------------+
706 706 #
707 707 # The actual abort is handled by `defineparents`
708 708 if len(root.parents()) <= 1:
709 709 # ancestors of <root> not ancestors of <dest>
710 710 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
711 711 [root.rev()]))
712 712 for r in detachset:
713 713 if r not in state:
714 714 state[r] = nullmerge
715 715 if len(roots) > 1:
716 716 # If we have multiple roots, we may have "hole" in the rebase set.
717 717 # Rebase roots that descend from those "hole" should not be detached as
718 718 # other root are. We use the special `revignored` to inform rebase that
719 719 # the revision should be ignored but that `defineparents` should search
720 720 # a rebase destination that make sense regarding rebased topology.
721 721 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
722 722 for ignored in set(rebasedomain) - set(rebaseset):
723 723 state[ignored] = revignored
724 724 return repo['.'].rev(), dest.rev(), state
725 725
726 726 def clearrebased(ui, repo, state, skipped, collapsedas=None):
727 727 """dispose of rebased revision at the end of the rebase
728 728
729 729 If `collapsedas` is not None, the rebase was a collapse whose result if the
730 730 `collapsedas` node."""
731 731 if obsolete._enabled:
732 732 markers = []
733 733 for rev, newrev in sorted(state.items()):
734 734 if newrev >= 0:
735 735 if rev in skipped:
736 736 succs = ()
737 737 elif collapsedas is not None:
738 738 succs = (repo[collapsedas],)
739 739 else:
740 740 succs = (repo[newrev],)
741 741 markers.append((repo[rev], succs))
742 742 if markers:
743 743 obsolete.createmarkers(repo, markers)
744 744 else:
745 745 rebased = [rev for rev in state if state[rev] > nullmerge]
746 746 if rebased:
747 747 stripped = []
748 748 for root in repo.set('roots(%ld)', rebased):
749 749 if set(repo.changelog.descendants([root.rev()])) - set(state):
750 750 ui.warn(_("warning: new changesets detected "
751 751 "on source branch, not stripping\n"))
752 752 else:
753 753 stripped.append(root.node())
754 754 if stripped:
755 755 # backup the old csets by default
756 756 repair.strip(ui, repo, stripped, "all")
757 757
758 758
759 759 def pullrebase(orig, ui, repo, *args, **opts):
760 760 'Call rebase after pull if the latter has been invoked with --rebase'
761 761 if opts.get('rebase'):
762 762 if opts.get('update'):
763 763 del opts['update']
764 764 ui.debug('--update and --rebase are not compatible, ignoring '
765 765 'the update flag\n')
766 766
767 767 movemarkfrom = repo['.'].node()
768 768 cmdutil.bailifchanged(repo)
769 769 revsprepull = len(repo)
770 770 origpostincoming = commands.postincoming
771 771 def _dummy(*args, **kwargs):
772 772 pass
773 773 commands.postincoming = _dummy
774 774 try:
775 775 orig(ui, repo, *args, **opts)
776 776 finally:
777 777 commands.postincoming = origpostincoming
778 778 revspostpull = len(repo)
779 779 if revspostpull > revsprepull:
780 780 # --rev option from pull conflict with rebase own --rev
781 781 # dropping it
782 782 if 'rev' in opts:
783 783 del opts['rev']
784 784 rebase(ui, repo, **opts)
785 785 branch = repo[None].branch()
786 786 dest = repo[branch].rev()
787 787 if dest != repo['.'].rev():
788 788 # there was nothing to rebase we force an update
789 789 hg.update(repo, dest)
790 790 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
791 791 ui.status(_("updating bookmark %s\n")
792 792 % repo._bookmarkcurrent)
793 793 else:
794 794 if opts.get('tool'):
795 795 raise util.Abort(_('--tool can only be used with --rebase'))
796 796 orig(ui, repo, *args, **opts)
797 797
798 798 def summaryhook(ui, repo):
799 799 if not os.path.exists(repo.join('rebasestate')):
800 800 return
801 801 state = restorestatus(repo)[2]
802 802 numrebased = len([i for i in state.itervalues() if i != -1])
803 803 # i18n: column positioning for "hg summary"
804 804 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
805 805 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
806 806 ui.label(_('%d remaining'), 'rebase.remaining') %
807 807 (len(state) - numrebased)))
808 808
809 809 def uisetup(ui):
810 810 'Replace pull with a decorator to provide --rebase option'
811 811 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
812 812 entry[1].append(('', 'rebase', None,
813 813 _("rebase working directory to branch head")))
814 814 entry[1].append(('t', 'tool', '',
815 815 _("specify merge tool for rebase")))
816 816 cmdutil.summaryhooks.add('rebase', summaryhook)
817 817 cmdutil.unfinishedstates.append(
818 818 ['rebasestate', False, False, _('rebase in progress'),
819 819 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
@@ -1,268 +1,267 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 > tglogp = log -G --template "{rev}:{phase} '{desc}' {branches}\n"
12 12 > EOF
13 13
14 14
15 15 $ hg init a
16 16 $ cd a
17 17
18 18 $ echo A > A
19 19 $ hg ci -Am A
20 20 adding A
21 21
22 22 $ echo B > B
23 23 $ hg ci -Am B
24 24 adding B
25 25
26 26 $ echo C >> A
27 27 $ hg ci -m C
28 28
29 29 $ hg up -q -C 0
30 30
31 31 $ echo D >> A
32 32 $ hg ci -m D
33 33 created new head
34 34
35 35 $ echo E > E
36 36 $ hg ci -Am E
37 37 adding E
38 38
39 39 $ cd ..
40 40
41 41
42 42 Changes during an interruption - continue:
43 43
44 44 $ hg clone -q -u . a a1
45 45 $ cd a1
46 46
47 47 $ hg tglog
48 48 @ 4: 'E'
49 49 |
50 50 o 3: 'D'
51 51 |
52 52 | o 2: 'C'
53 53 | |
54 54 | o 1: 'B'
55 55 |/
56 56 o 0: 'A'
57 57
58 58 Rebasing B onto E:
59 59
60 60 $ hg rebase -s 1 -d 4
61 61 merging A
62 62 warning: conflicts during merge.
63 63 merging A incomplete! (edit conflicts, then use 'hg resolve --mark')
64 64 unresolved conflicts (see hg resolve, then hg rebase --continue)
65 65 [1]
66 66
67 67 Force a commit on C during the interruption:
68 68
69 69 $ hg up -q -C 2 --config 'extensions.rebase=!'
70 70
71 71 $ echo 'Extra' > Extra
72 72 $ hg add Extra
73 73 $ hg ci -m 'Extra' --config 'extensions.rebase=!'
74 74
75 75 Force this commit onto secret phase
76 76
77 77 $ hg phase --force --secret 6
78 78
79 79 $ hg tglogp
80 80 @ 6:secret 'Extra'
81 81 |
82 82 | o 5:draft 'B'
83 83 | |
84 84 | o 4:draft 'E'
85 85 | |
86 86 | o 3:draft 'D'
87 87 | |
88 88 o | 2:draft 'C'
89 89 | |
90 90 o | 1:draft 'B'
91 91 |/
92 92 o 0:draft 'A'
93 93
94 94 Resume the rebasing:
95 95
96 96 $ hg rebase --continue
97 97 merging A
98 98 warning: conflicts during merge.
99 99 merging A incomplete! (edit conflicts, then use 'hg resolve --mark')
100 100 unresolved conflicts (see hg resolve, then hg rebase --continue)
101 101 [1]
102 102
103 103 Solve the conflict and go on:
104 104
105 105 $ echo 'conflict solved' > A
106 106 $ rm A.orig
107 107 $ hg resolve -m A
108 108
109 109 $ hg rebase --continue
110 110 warning: new changesets detected on source branch, not stripping
111 111
112 112 $ hg tglogp
113 113 @ 7:draft 'C'
114 114 |
115 115 | o 6:secret 'Extra'
116 116 | |
117 117 o | 5:draft 'B'
118 118 | |
119 119 o | 4:draft 'E'
120 120 | |
121 121 o | 3:draft 'D'
122 122 | |
123 123 | o 2:draft 'C'
124 124 | |
125 125 | o 1:draft 'B'
126 126 |/
127 127 o 0:draft 'A'
128 128
129 129 $ cd ..
130 130
131 131
132 132 Changes during an interruption - abort:
133 133
134 134 $ hg clone -q -u . a a2
135 135 $ cd a2
136 136
137 137 $ hg tglog
138 138 @ 4: 'E'
139 139 |
140 140 o 3: 'D'
141 141 |
142 142 | o 2: 'C'
143 143 | |
144 144 | o 1: 'B'
145 145 |/
146 146 o 0: 'A'
147 147
148 148 Rebasing B onto E:
149 149
150 150 $ hg rebase -s 1 -d 4
151 151 merging A
152 152 warning: conflicts during merge.
153 153 merging A incomplete! (edit conflicts, then use 'hg resolve --mark')
154 154 unresolved conflicts (see hg resolve, then hg rebase --continue)
155 155 [1]
156 156
157 157 Force a commit on B' during the interruption:
158 158
159 159 $ hg up -q -C 5 --config 'extensions.rebase=!'
160 160
161 161 $ echo 'Extra' > Extra
162 162 $ hg add Extra
163 163 $ hg ci -m 'Extra' --config 'extensions.rebase=!'
164 164
165 165 $ hg tglog
166 166 @ 6: 'Extra'
167 167 |
168 168 o 5: 'B'
169 169 |
170 170 o 4: 'E'
171 171 |
172 172 o 3: 'D'
173 173 |
174 174 | o 2: 'C'
175 175 | |
176 176 | o 1: 'B'
177 177 |/
178 178 o 0: 'A'
179 179
180 180 Abort the rebasing:
181 181
182 182 $ hg rebase --abort
183 183 warning: new changesets detected on target branch, can't abort
184 184 [255]
185 185
186 186 $ hg tglog
187 187 @ 6: 'Extra'
188 188 |
189 189 o 5: 'B'
190 190 |
191 191 o 4: 'E'
192 192 |
193 193 o 3: 'D'
194 194 |
195 195 | o 2: 'C'
196 196 | |
197 197 | o 1: 'B'
198 198 |/
199 199 o 0: 'A'
200 200
201 201 $ cd ..
202 202
203 203 Changes during an interruption - abort (again):
204 204
205 205 $ hg clone -q -u . a a3
206 206 $ cd a3
207 207
208 208 $ hg tglogp
209 209 @ 4:draft 'E'
210 210 |
211 211 o 3:draft 'D'
212 212 |
213 213 | o 2:draft 'C'
214 214 | |
215 215 | o 1:draft 'B'
216 216 |/
217 217 o 0:draft 'A'
218 218
219 219 Rebasing B onto E:
220 220
221 221 $ hg rebase -s 1 -d 4
222 222 merging A
223 223 warning: conflicts during merge.
224 224 merging A incomplete! (edit conflicts, then use 'hg resolve --mark')
225 225 unresolved conflicts (see hg resolve, then hg rebase --continue)
226 226 [1]
227 227
228 228 Change phase on B and B'
229 229
230 230 $ hg up -q -C 5 --config 'extensions.rebase=!'
231 231 $ hg phase --public 1
232 232 $ hg phase --public 5
233 233 $ hg phase --secret -f 2
234 234
235 235 $ hg tglogp
236 236 @ 5:public 'B'
237 237 |
238 238 o 4:public 'E'
239 239 |
240 240 o 3:public 'D'
241 241 |
242 242 | o 2:secret 'C'
243 243 | |
244 244 | o 1:public 'B'
245 245 |/
246 246 o 0:public 'A'
247 247
248 248 Abort the rebasing:
249 249
250 250 $ hg rebase --abort
251 abort: can't abort rebase due to immutable changesets 45396c49d53b
252 (see hg help phases for details)
253 [255]
251 warning: can't clean up immutable changesets 45396c49d53b
252 rebase aborted
254 253
255 254 $ hg tglogp
256 255 @ 5:public 'B'
257 256 |
258 257 o 4:public 'E'
259 258 |
260 259 o 3:public 'D'
261 260 |
262 261 | o 2:secret 'C'
263 262 | |
264 263 | o 1:public 'B'
265 264 |/
266 265 o 0:public 'A'
267 266
268 267 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now