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