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