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