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