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