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