##// END OF EJS Templates
rebase: perform update through the 'update' command...
Pierre-Yves David -
r28118:0e3835c7 default
parent child Browse files
Show More
@@ -1,1283 +1,1278 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 https://mercurial-scm.org/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 from mercurial import copies, repoview, revset
19 from mercurial import copies, destutil, repoview, revset
20 20 from mercurial.commands import templateopts
21 21 from mercurial.node import nullrev, nullid, hex, short
22 22 from mercurial.lock import release
23 23 from mercurial.i18n import _
24 24 import os, errno
25 25
26 26 # The following constants are used throughout the rebase module. The ordering of
27 27 # their values must be maintained.
28 28
29 29 # Indicates that a revision needs to be rebased
30 30 revtodo = -1
31 31 nullmerge = -2
32 32 revignored = -3
33 33 # successor in rebase destination
34 34 revprecursor = -4
35 35 # plain prune (no successor)
36 36 revpruned = -5
37 37 revskipped = (revignored, revprecursor, revpruned)
38 38
39 39 cmdtable = {}
40 40 command = cmdutil.command(cmdtable)
41 41 # Note for extension authors: ONLY specify testedwith = 'internal' for
42 42 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
43 43 # be specifying the version(s) of Mercurial they are tested with, or
44 44 # leave the attribute unspecified.
45 45 testedwith = 'internal'
46 46
47 47 def _nothingtorebase():
48 48 return 1
49 49
50 50 def _savegraft(ctx, extra):
51 51 s = ctx.extra().get('source', None)
52 52 if s is not None:
53 53 extra['source'] = s
54 54 s = ctx.extra().get('intermediate-source', None)
55 55 if s is not None:
56 56 extra['intermediate-source'] = s
57 57
58 58 def _savebranch(ctx, extra):
59 59 extra['branch'] = ctx.branch()
60 60
61 61 def _makeextrafn(copiers):
62 62 """make an extrafn out of the given copy-functions.
63 63
64 64 A copy function takes a context and an extra dict, and mutates the
65 65 extra dict as needed based on the given context.
66 66 """
67 67 def extrafn(ctx, extra):
68 68 for c in copiers:
69 69 c(ctx, extra)
70 70 return extrafn
71 71
72 72 def _destrebase(repo):
73 73 # Destination defaults to the latest revision in the
74 74 # current branch
75 75 branch = repo[None].branch()
76 76 return repo[branch].rev()
77 77
78 78 revsetpredicate = revset.extpredicate()
79 79
80 80 @revsetpredicate('_destrebase')
81 81 def _revsetdestrebase(repo, subset, x):
82 82 # ``_rebasedefaultdest()``
83 83
84 84 # default destination for rebase.
85 85 # # XXX: Currently private because I expect the signature to change.
86 86 # # XXX: - taking rev as arguments,
87 87 # # XXX: - bailing out in case of ambiguity vs returning all data.
88 88 # # XXX: - probably merging with the merge destination.
89 89 # i18n: "_rebasedefaultdest" is a keyword
90 90 revset.getargs(x, 0, 0, _("_rebasedefaultdest takes no arguments"))
91 91 return subset & revset.baseset([_destrebase(repo)])
92 92
93 93 @command('rebase',
94 94 [('s', 'source', '',
95 95 _('rebase the specified changeset and descendants'), _('REV')),
96 96 ('b', 'base', '',
97 97 _('rebase everything from branching point of specified changeset'),
98 98 _('REV')),
99 99 ('r', 'rev', [],
100 100 _('rebase these revisions'),
101 101 _('REV')),
102 102 ('d', 'dest', '',
103 103 _('rebase onto the specified changeset'), _('REV')),
104 104 ('', 'collapse', False, _('collapse the rebased changesets')),
105 105 ('m', 'message', '',
106 106 _('use text as collapse commit message'), _('TEXT')),
107 107 ('e', 'edit', False, _('invoke editor on commit messages')),
108 108 ('l', 'logfile', '',
109 109 _('read collapse commit message from file'), _('FILE')),
110 110 ('k', 'keep', False, _('keep original changesets')),
111 111 ('', 'keepbranches', False, _('keep original branch names')),
112 112 ('D', 'detach', False, _('(DEPRECATED)')),
113 113 ('i', 'interactive', False, _('(DEPRECATED)')),
114 114 ('t', 'tool', '', _('specify merge tool')),
115 115 ('c', 'continue', False, _('continue an interrupted rebase')),
116 116 ('a', 'abort', False, _('abort an interrupted rebase'))] +
117 117 templateopts,
118 118 _('[-s REV | -b REV] [-d REV] [OPTION]'))
119 119 def rebase(ui, repo, **opts):
120 120 """move changeset (and descendants) to a different branch
121 121
122 122 Rebase uses repeated merging to graft changesets from one part of
123 123 history (the source) onto another (the destination). This can be
124 124 useful for linearizing *local* changes relative to a master
125 125 development tree.
126 126
127 127 Published commits cannot be rebased (see :hg:`help phases`).
128 128 To copy commits, see :hg:`help graft`.
129 129
130 130 If you don't specify a destination changeset (``-d/--dest``),
131 131 rebase uses the current branch tip as the destination. (The
132 132 destination changeset is not modified by rebasing, but new
133 133 changesets are added as its descendants.)
134 134
135 135 Here are the ways to select changesets:
136 136
137 137 1. Explicitly select them using ``--rev``.
138 138
139 139 2. Use ``--source`` to select a root changeset and include all of its
140 140 descendants.
141 141
142 142 3. Use ``--base`` to select a changeset; rebase will find ancestors
143 143 and their descendants which are not also ancestors of the destination.
144 144
145 145 4. If you do not specify any of ``--rev``, ``source``, or ``--base``,
146 146 rebase will use ``--base .`` as above.
147 147
148 148 Rebase will destroy original changesets unless you use ``--keep``.
149 149 It will also move your bookmarks (even if you do).
150 150
151 151 Some changesets may be dropped if they do not contribute changes
152 152 (e.g. merges from the destination branch).
153 153
154 154 Unlike ``merge``, rebase will do nothing if you are at the branch tip of
155 155 a named branch with two heads. You will need to explicitly specify source
156 156 and/or destination.
157 157
158 158 If you need to use a tool to automate merge/conflict decisions, you
159 159 can specify one with ``--tool``, see :hg:`help merge-tools`.
160 160 As a caveat: the tool will not be used to mediate when a file was
161 161 deleted, there is no hook presently available for this.
162 162
163 163 If a rebase is interrupted to manually resolve a conflict, it can be
164 164 continued with --continue/-c or aborted with --abort/-a.
165 165
166 166 .. container:: verbose
167 167
168 168 Examples:
169 169
170 170 - move "local changes" (current commit back to branching point)
171 171 to the current branch tip after a pull::
172 172
173 173 hg rebase
174 174
175 175 - move a single changeset to the stable branch::
176 176
177 177 hg rebase -r 5f493448 -d stable
178 178
179 179 - splice a commit and all its descendants onto another part of history::
180 180
181 181 hg rebase --source c0c3 --dest 4cf9
182 182
183 183 - rebase everything on a branch marked by a bookmark onto the
184 184 default branch::
185 185
186 186 hg rebase --base myfeature --dest default
187 187
188 188 - collapse a sequence of changes into a single commit::
189 189
190 190 hg rebase --collapse -r 1520:1525 -d .
191 191
192 192 - move a named branch while preserving its name::
193 193
194 194 hg rebase -r "branch(featureX)" -d 1.3 --keepbranches
195 195
196 196 Returns 0 on success, 1 if nothing to rebase or there are
197 197 unresolved conflicts.
198 198
199 199 """
200 200 originalwd = target = None
201 201 activebookmark = None
202 202 external = nullrev
203 203 # Mapping between the old revision id and either what is the new rebased
204 204 # revision or what needs to be done with the old revision. The state dict
205 205 # will be what contains most of the rebase progress state.
206 206 state = {}
207 207 skipped = set()
208 208 targetancestors = set()
209 209
210 210
211 211 lock = wlock = None
212 212 try:
213 213 wlock = repo.wlock()
214 214 lock = repo.lock()
215 215
216 216 # Validate input and define rebasing points
217 217 destf = opts.get('dest', None)
218 218 srcf = opts.get('source', None)
219 219 basef = opts.get('base', None)
220 220 revf = opts.get('rev', [])
221 221 contf = opts.get('continue')
222 222 abortf = opts.get('abort')
223 223 collapsef = opts.get('collapse', False)
224 224 collapsemsg = cmdutil.logmessage(ui, opts)
225 225 date = opts.get('date', None)
226 226 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
227 227 extrafns = [_savegraft]
228 228 if e:
229 229 extrafns = [e]
230 230 keepf = opts.get('keep', False)
231 231 keepbranchesf = opts.get('keepbranches', False)
232 232 # keepopen is not meant for use on the command line, but by
233 233 # other extensions
234 234 keepopen = opts.get('keepopen', False)
235 235
236 236 if opts.get('interactive'):
237 237 try:
238 238 if extensions.find('histedit'):
239 239 enablehistedit = ''
240 240 except KeyError:
241 241 enablehistedit = " --config extensions.histedit="
242 242 help = "hg%s help -e histedit" % enablehistedit
243 243 msg = _("interactive history editing is supported by the "
244 244 "'histedit' extension (see \"%s\")") % help
245 245 raise error.Abort(msg)
246 246
247 247 if collapsemsg and not collapsef:
248 248 raise error.Abort(
249 249 _('message can only be specified with collapse'))
250 250
251 251 if contf or abortf:
252 252 if contf and abortf:
253 253 raise error.Abort(_('cannot use both abort and continue'))
254 254 if collapsef:
255 255 raise error.Abort(
256 256 _('cannot use collapse with continue or abort'))
257 257 if srcf or basef or destf:
258 258 raise error.Abort(
259 259 _('abort and continue do not allow specifying revisions'))
260 260 if abortf and opts.get('tool', False):
261 261 ui.warn(_('tool option will be ignored\n'))
262 262
263 263 try:
264 264 (originalwd, target, state, skipped, collapsef, keepf,
265 265 keepbranchesf, external, activebookmark) = restorestatus(repo)
266 266 except error.RepoLookupError:
267 267 if abortf:
268 268 clearstatus(repo)
269 269 repo.ui.warn(_('rebase aborted (no revision is removed,'
270 270 ' only broken state is cleared)\n'))
271 271 return 0
272 272 else:
273 273 msg = _('cannot continue inconsistent rebase')
274 274 hint = _('use "hg rebase --abort" to clear broken state')
275 275 raise error.Abort(msg, hint=hint)
276 276 if abortf:
277 277 return abort(repo, originalwd, target, state,
278 278 activebookmark=activebookmark)
279 279 else:
280 280 if srcf and basef:
281 281 raise error.Abort(_('cannot specify both a '
282 282 'source and a base'))
283 283 if revf and basef:
284 284 raise error.Abort(_('cannot specify both a '
285 285 'revision and a base'))
286 286 if revf and srcf:
287 287 raise error.Abort(_('cannot specify both a '
288 288 'revision and a source'))
289 289
290 290 cmdutil.checkunfinished(repo)
291 291 cmdutil.bailifchanged(repo)
292 292
293 293 if destf:
294 294 dest = scmutil.revsingle(repo, destf)
295 295 else:
296 296 dest = repo[_destrebase(repo)]
297 297 destf = str(dest)
298 298
299 299 if revf:
300 300 rebaseset = scmutil.revrange(repo, revf)
301 301 if not rebaseset:
302 302 ui.status(_('empty "rev" revision set - '
303 303 'nothing to rebase\n'))
304 304 return _nothingtorebase()
305 305 elif srcf:
306 306 src = scmutil.revrange(repo, [srcf])
307 307 if not src:
308 308 ui.status(_('empty "source" revision set - '
309 309 'nothing to rebase\n'))
310 310 return _nothingtorebase()
311 311 rebaseset = repo.revs('(%ld)::', src)
312 312 assert rebaseset
313 313 else:
314 314 base = scmutil.revrange(repo, [basef or '.'])
315 315 if not base:
316 316 ui.status(_('empty "base" revision set - '
317 317 "can't compute rebase set\n"))
318 318 return _nothingtorebase()
319 319 commonanc = repo.revs('ancestor(%ld, %d)', base, dest).first()
320 320 if commonanc is not None:
321 321 rebaseset = repo.revs('(%d::(%ld) - %d)::',
322 322 commonanc, base, commonanc)
323 323 else:
324 324 rebaseset = []
325 325
326 326 if not rebaseset:
327 327 # transform to list because smartsets are not comparable to
328 328 # lists. This should be improved to honor laziness of
329 329 # smartset.
330 330 if list(base) == [dest.rev()]:
331 331 if basef:
332 332 ui.status(_('nothing to rebase - %s is both "base"'
333 333 ' and destination\n') % dest)
334 334 else:
335 335 ui.status(_('nothing to rebase - working directory '
336 336 'parent is also destination\n'))
337 337 elif not repo.revs('%ld - ::%d', base, dest):
338 338 if basef:
339 339 ui.status(_('nothing to rebase - "base" %s is '
340 340 'already an ancestor of destination '
341 341 '%s\n') %
342 342 ('+'.join(str(repo[r]) for r in base),
343 343 dest))
344 344 else:
345 345 ui.status(_('nothing to rebase - working '
346 346 'directory parent is already an '
347 347 'ancestor of destination %s\n') % dest)
348 348 else: # can it happen?
349 349 ui.status(_('nothing to rebase from %s to %s\n') %
350 350 ('+'.join(str(repo[r]) for r in base), dest))
351 351 return _nothingtorebase()
352 352
353 353 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
354 354 if (not (keepf or allowunstable)
355 355 and repo.revs('first(children(%ld) - %ld)',
356 356 rebaseset, rebaseset)):
357 357 raise error.Abort(
358 358 _("can't remove original changesets with"
359 359 " unrebased descendants"),
360 360 hint=_('use --keep to keep original changesets'))
361 361
362 362 obsoletenotrebased = {}
363 363 if ui.configbool('experimental', 'rebaseskipobsolete'):
364 364 rebasesetrevs = set(rebaseset)
365 365 rebaseobsrevs = _filterobsoleterevs(repo, rebasesetrevs)
366 366 obsoletenotrebased = _computeobsoletenotrebased(repo,
367 367 rebaseobsrevs,
368 368 dest)
369 369 rebaseobsskipped = set(obsoletenotrebased)
370 370
371 371 # Obsolete node with successors not in dest leads to divergence
372 372 divergenceok = ui.configbool('rebase',
373 373 'allowdivergence')
374 374 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
375 375
376 376 if divergencebasecandidates and not divergenceok:
377 377 msg = _("this rebase will cause divergence")
378 378 h = _("to force the rebase please set "
379 379 "rebase.allowdivergence=True")
380 380 raise error.Abort(msg, hint=h)
381 381
382 382 # - plain prune (no successor) changesets are rebased
383 383 # - split changesets are not rebased if at least one of the
384 384 # changeset resulting from the split is an ancestor of dest
385 385 rebaseset = rebasesetrevs - rebaseobsskipped
386 386 if rebasesetrevs and not rebaseset:
387 387 msg = _('all requested changesets have equivalents '
388 388 'or were marked as obsolete')
389 389 hint = _('to force the rebase, set the config '
390 390 'experimental.rebaseskipobsolete to False')
391 391 raise error.Abort(msg, hint=hint)
392 392
393 393 result = buildstate(repo, dest, rebaseset, collapsef,
394 394 obsoletenotrebased)
395 395
396 396 if not result:
397 397 # Empty state built, nothing to rebase
398 398 ui.status(_('nothing to rebase\n'))
399 399 return _nothingtorebase()
400 400
401 401 root = min(rebaseset)
402 402 if not keepf and not repo[root].mutable():
403 403 raise error.Abort(_("can't rebase public changeset %s")
404 404 % repo[root],
405 405 hint=_('see "hg help phases" for details'))
406 406
407 407 originalwd, target, state = result
408 408 if collapsef:
409 409 targetancestors = repo.changelog.ancestors([target],
410 410 inclusive=True)
411 411 external = externalparent(repo, state, targetancestors)
412 412
413 413 if dest.closesbranch() and not keepbranchesf:
414 414 ui.status(_('reopening closed branch head %s\n') % dest)
415 415
416 416 if keepbranchesf:
417 417 # insert _savebranch at the start of extrafns so if
418 418 # there's a user-provided extrafn it can clobber branch if
419 419 # desired
420 420 extrafns.insert(0, _savebranch)
421 421 if collapsef:
422 422 branches = set()
423 423 for rev in state:
424 424 branches.add(repo[rev].branch())
425 425 if len(branches) > 1:
426 426 raise error.Abort(_('cannot collapse multiple named '
427 427 'branches'))
428 428
429 429 # Rebase
430 430 if not targetancestors:
431 431 targetancestors = repo.changelog.ancestors([target], inclusive=True)
432 432
433 433 # Keep track of the current bookmarks in order to reset them later
434 434 currentbookmarks = repo._bookmarks.copy()
435 435 activebookmark = activebookmark or repo._activebookmark
436 436 if activebookmark:
437 437 bookmarks.deactivate(repo)
438 438
439 439 extrafn = _makeextrafn(extrafns)
440 440
441 441 sortedstate = sorted(state)
442 442 total = len(sortedstate)
443 443 pos = 0
444 444 for rev in sortedstate:
445 445 ctx = repo[rev]
446 446 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
447 447 ctx.description().split('\n', 1)[0])
448 448 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
449 449 if names:
450 450 desc += ' (%s)' % ' '.join(names)
451 451 pos += 1
452 452 if state[rev] == revtodo:
453 453 ui.status(_('rebasing %s\n') % desc)
454 454 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
455 455 _('changesets'), total)
456 456 p1, p2, base = defineparents(repo, rev, target, state,
457 457 targetancestors)
458 458 storestatus(repo, originalwd, target, state, collapsef, keepf,
459 459 keepbranchesf, external, activebookmark)
460 460 if len(repo[None].parents()) == 2:
461 461 repo.ui.debug('resuming interrupted rebase\n')
462 462 else:
463 463 try:
464 464 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
465 465 'rebase')
466 466 stats = rebasenode(repo, rev, p1, base, state,
467 467 collapsef, target)
468 468 if stats and stats[3] > 0:
469 469 raise error.InterventionRequired(
470 470 _('unresolved conflicts (see hg '
471 471 'resolve, then hg rebase --continue)'))
472 472 finally:
473 473 ui.setconfig('ui', 'forcemerge', '', 'rebase')
474 474 if not collapsef:
475 475 merging = p2 != nullrev
476 476 editform = cmdutil.mergeeditform(merging, 'rebase')
477 477 editor = cmdutil.getcommiteditor(editform=editform, **opts)
478 478 newnode = concludenode(repo, rev, p1, p2, extrafn=extrafn,
479 479 editor=editor,
480 480 keepbranches=keepbranchesf,
481 481 date=date)
482 482 else:
483 483 # Skip commit if we are collapsing
484 484 repo.dirstate.beginparentchange()
485 485 repo.setparents(repo[p1].node())
486 486 repo.dirstate.endparentchange()
487 487 newnode = None
488 488 # Update the state
489 489 if newnode is not None:
490 490 state[rev] = repo[newnode].rev()
491 491 ui.debug('rebased as %s\n' % short(newnode))
492 492 else:
493 493 if not collapsef:
494 494 ui.warn(_('note: rebase of %d:%s created no changes '
495 495 'to commit\n') % (rev, ctx))
496 496 skipped.add(rev)
497 497 state[rev] = p1
498 498 ui.debug('next revision set to %s\n' % p1)
499 499 elif state[rev] == nullmerge:
500 500 ui.debug('ignoring null merge rebase of %s\n' % rev)
501 501 elif state[rev] == revignored:
502 502 ui.status(_('not rebasing ignored %s\n') % desc)
503 503 elif state[rev] == revprecursor:
504 504 targetctx = repo[obsoletenotrebased[rev]]
505 505 desctarget = '%d:%s "%s"' % (targetctx.rev(), targetctx,
506 506 targetctx.description().split('\n', 1)[0])
507 507 msg = _('note: not rebasing %s, already in destination as %s\n')
508 508 ui.status(msg % (desc, desctarget))
509 509 elif state[rev] == revpruned:
510 510 msg = _('note: not rebasing %s, it has no successor\n')
511 511 ui.status(msg % desc)
512 512 else:
513 513 ui.status(_('already rebased %s as %s\n') %
514 514 (desc, repo[state[rev]]))
515 515
516 516 ui.progress(_('rebasing'), None)
517 517 ui.note(_('rebase merging completed\n'))
518 518
519 519 if collapsef and not keepopen:
520 520 p1, p2, _base = defineparents(repo, min(state), target,
521 521 state, targetancestors)
522 522 editopt = opts.get('edit')
523 523 editform = 'rebase.collapse'
524 524 if collapsemsg:
525 525 commitmsg = collapsemsg
526 526 else:
527 527 commitmsg = 'Collapsed revision'
528 528 for rebased in state:
529 529 if rebased not in skipped and state[rebased] > nullmerge:
530 530 commitmsg += '\n* %s' % repo[rebased].description()
531 531 editopt = True
532 532 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
533 533 newnode = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
534 534 extrafn=extrafn, editor=editor,
535 535 keepbranches=keepbranchesf,
536 536 date=date)
537 537 if newnode is None:
538 538 newrev = target
539 539 else:
540 540 newrev = repo[newnode].rev()
541 541 for oldrev in state.iterkeys():
542 542 if state[oldrev] > nullmerge:
543 543 state[oldrev] = newrev
544 544
545 545 if 'qtip' in repo.tags():
546 546 updatemq(repo, state, skipped, **opts)
547 547
548 548 if currentbookmarks:
549 549 # Nodeids are needed to reset bookmarks
550 550 nstate = {}
551 551 for k, v in state.iteritems():
552 552 if v > nullmerge:
553 553 nstate[repo[k].node()] = repo[v].node()
554 554 # XXX this is the same as dest.node() for the non-continue path --
555 555 # this should probably be cleaned up
556 556 targetnode = repo[target].node()
557 557
558 558 # restore original working directory
559 559 # (we do this before stripping)
560 560 newwd = state.get(originalwd, originalwd)
561 561 if newwd < 0:
562 562 # original directory is a parent of rebase set root or ignored
563 563 newwd = originalwd
564 564 if newwd not in [c.rev() for c in repo[None].parents()]:
565 565 ui.note(_("update back to initial working directory parent\n"))
566 566 hg.updaterepo(repo, newwd, False)
567 567
568 568 if not keepf:
569 569 collapsedas = None
570 570 if collapsef:
571 571 collapsedas = newnode
572 572 clearrebased(ui, repo, state, skipped, collapsedas)
573 573
574 574 with repo.transaction('bookmark') as tr:
575 575 if currentbookmarks:
576 576 updatebookmarks(repo, targetnode, nstate, currentbookmarks, tr)
577 577 if activebookmark not in repo._bookmarks:
578 578 # active bookmark was divergent one and has been deleted
579 579 activebookmark = None
580 580 clearstatus(repo)
581 581
582 582 ui.note(_("rebase completed\n"))
583 583 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
584 584 if skipped:
585 585 ui.note(_("%d revisions have been skipped\n") % len(skipped))
586 586
587 587 if (activebookmark and
588 588 repo['.'].node() == repo._bookmarks[activebookmark]):
589 589 bookmarks.activate(repo, activebookmark)
590 590
591 591 finally:
592 592 release(lock, wlock)
593 593
594 594 def externalparent(repo, state, targetancestors):
595 595 """Return the revision that should be used as the second parent
596 596 when the revisions in state is collapsed on top of targetancestors.
597 597 Abort if there is more than one parent.
598 598 """
599 599 parents = set()
600 600 source = min(state)
601 601 for rev in state:
602 602 if rev == source:
603 603 continue
604 604 for p in repo[rev].parents():
605 605 if (p.rev() not in state
606 606 and p.rev() not in targetancestors):
607 607 parents.add(p.rev())
608 608 if not parents:
609 609 return nullrev
610 610 if len(parents) == 1:
611 611 return parents.pop()
612 612 raise error.Abort(_('unable to collapse on top of %s, there is more '
613 613 'than one external parent: %s') %
614 614 (max(targetancestors),
615 615 ', '.join(str(p) for p in sorted(parents))))
616 616
617 617 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None,
618 618 keepbranches=False, date=None):
619 619 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
620 620 but also store useful information in extra.
621 621 Return node of committed revision.'''
622 622 dsguard = cmdutil.dirstateguard(repo, 'rebase')
623 623 try:
624 624 repo.setparents(repo[p1].node(), repo[p2].node())
625 625 ctx = repo[rev]
626 626 if commitmsg is None:
627 627 commitmsg = ctx.description()
628 628 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
629 629 extra = {'rebase_source': ctx.hex()}
630 630 if extrafn:
631 631 extrafn(ctx, extra)
632 632
633 633 backup = repo.ui.backupconfig('phases', 'new-commit')
634 634 try:
635 635 targetphase = max(ctx.phase(), phases.draft)
636 636 repo.ui.setconfig('phases', 'new-commit', targetphase, 'rebase')
637 637 if keepbranch:
638 638 repo.ui.setconfig('ui', 'allowemptycommit', True)
639 639 # Commit might fail if unresolved files exist
640 640 if date is None:
641 641 date = ctx.date()
642 642 newnode = repo.commit(text=commitmsg, user=ctx.user(),
643 643 date=date, extra=extra, editor=editor)
644 644 finally:
645 645 repo.ui.restoreconfig(backup)
646 646
647 647 repo.dirstate.setbranch(repo[newnode].branch())
648 648 dsguard.close()
649 649 return newnode
650 650 finally:
651 651 release(dsguard)
652 652
653 653 def rebasenode(repo, rev, p1, base, state, collapse, target):
654 654 'Rebase a single revision rev on top of p1 using base as merge ancestor'
655 655 # Merge phase
656 656 # Update to target and merge it with local
657 657 if repo['.'].rev() != p1:
658 658 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
659 659 merge.update(repo, p1, False, True)
660 660 else:
661 661 repo.ui.debug(" already in target\n")
662 662 repo.dirstate.write(repo.currenttransaction())
663 663 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
664 664 if base is not None:
665 665 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
666 666 # When collapsing in-place, the parent is the common ancestor, we
667 667 # have to allow merging with it.
668 668 stats = merge.update(repo, rev, True, True, base, collapse,
669 669 labels=['dest', 'source'])
670 670 if collapse:
671 671 copies.duplicatecopies(repo, rev, target)
672 672 else:
673 673 # If we're not using --collapse, we need to
674 674 # duplicate copies between the revision we're
675 675 # rebasing and its first parent, but *not*
676 676 # duplicate any copies that have already been
677 677 # performed in the destination.
678 678 p1rev = repo[rev].p1().rev()
679 679 copies.duplicatecopies(repo, rev, p1rev, skiprev=target)
680 680 return stats
681 681
682 682 def nearestrebased(repo, rev, state):
683 683 """return the nearest ancestors of rev in the rebase result"""
684 684 rebased = [r for r in state if state[r] > nullmerge]
685 685 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
686 686 if candidates:
687 687 return state[candidates.first()]
688 688 else:
689 689 return None
690 690
691 691 def defineparents(repo, rev, target, state, targetancestors):
692 692 'Return the new parent relationship of the revision that will be rebased'
693 693 parents = repo[rev].parents()
694 694 p1 = p2 = nullrev
695 695
696 696 p1n = parents[0].rev()
697 697 if p1n in targetancestors:
698 698 p1 = target
699 699 elif p1n in state:
700 700 if state[p1n] == nullmerge:
701 701 p1 = target
702 702 elif state[p1n] in revskipped:
703 703 p1 = nearestrebased(repo, p1n, state)
704 704 if p1 is None:
705 705 p1 = target
706 706 else:
707 707 p1 = state[p1n]
708 708 else: # p1n external
709 709 p1 = target
710 710 p2 = p1n
711 711
712 712 if len(parents) == 2 and parents[1].rev() not in targetancestors:
713 713 p2n = parents[1].rev()
714 714 # interesting second parent
715 715 if p2n in state:
716 716 if p1 == target: # p1n in targetancestors or external
717 717 p1 = state[p2n]
718 718 elif state[p2n] in revskipped:
719 719 p2 = nearestrebased(repo, p2n, state)
720 720 if p2 is None:
721 721 # no ancestors rebased yet, detach
722 722 p2 = target
723 723 else:
724 724 p2 = state[p2n]
725 725 else: # p2n external
726 726 if p2 != nullrev: # p1n external too => rev is a merged revision
727 727 raise error.Abort(_('cannot use revision %d as base, result '
728 728 'would have 3 parents') % rev)
729 729 p2 = p2n
730 730 repo.ui.debug(" future parents are %d and %d\n" %
731 731 (repo[p1].rev(), repo[p2].rev()))
732 732
733 733 if not any(p.rev() in state for p in parents):
734 734 # Case (1) root changeset of a non-detaching rebase set.
735 735 # Let the merge mechanism find the base itself.
736 736 base = None
737 737 elif not repo[rev].p2():
738 738 # Case (2) detaching the node with a single parent, use this parent
739 739 base = repo[rev].p1().rev()
740 740 else:
741 741 # Assuming there is a p1, this is the case where there also is a p2.
742 742 # We are thus rebasing a merge and need to pick the right merge base.
743 743 #
744 744 # Imagine we have:
745 745 # - M: current rebase revision in this step
746 746 # - A: one parent of M
747 747 # - B: other parent of M
748 748 # - D: destination of this merge step (p1 var)
749 749 #
750 750 # Consider the case where D is a descendant of A or B and the other is
751 751 # 'outside'. In this case, the right merge base is the D ancestor.
752 752 #
753 753 # An informal proof, assuming A is 'outside' and B is the D ancestor:
754 754 #
755 755 # If we pick B as the base, the merge involves:
756 756 # - changes from B to M (actual changeset payload)
757 757 # - changes from B to D (induced by rebase) as D is a rebased
758 758 # version of B)
759 759 # Which exactly represent the rebase operation.
760 760 #
761 761 # If we pick A as the base, the merge involves:
762 762 # - changes from A to M (actual changeset payload)
763 763 # - changes from A to D (with include changes between unrelated A and B
764 764 # plus changes induced by rebase)
765 765 # Which does not represent anything sensible and creates a lot of
766 766 # conflicts. A is thus not the right choice - B is.
767 767 #
768 768 # Note: The base found in this 'proof' is only correct in the specified
769 769 # case. This base does not make sense if is not D a descendant of A or B
770 770 # or if the other is not parent 'outside' (especially not if the other
771 771 # parent has been rebased). The current implementation does not
772 772 # make it feasible to consider different cases separately. In these
773 773 # other cases we currently just leave it to the user to correctly
774 774 # resolve an impossible merge using a wrong ancestor.
775 775 for p in repo[rev].parents():
776 776 if state.get(p.rev()) == p1:
777 777 base = p.rev()
778 778 break
779 779 else: # fallback when base not found
780 780 base = None
781 781
782 782 # Raise because this function is called wrong (see issue 4106)
783 783 raise AssertionError('no base found to rebase on '
784 784 '(defineparents called wrong)')
785 785 return p1, p2, base
786 786
787 787 def isagitpatch(repo, patchname):
788 788 'Return true if the given patch is in git format'
789 789 mqpatch = os.path.join(repo.mq.path, patchname)
790 790 for line in patch.linereader(file(mqpatch, 'rb')):
791 791 if line.startswith('diff --git'):
792 792 return True
793 793 return False
794 794
795 795 def updatemq(repo, state, skipped, **opts):
796 796 'Update rebased mq patches - finalize and then import them'
797 797 mqrebase = {}
798 798 mq = repo.mq
799 799 original_series = mq.fullseries[:]
800 800 skippedpatches = set()
801 801
802 802 for p in mq.applied:
803 803 rev = repo[p.node].rev()
804 804 if rev in state:
805 805 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
806 806 (rev, p.name))
807 807 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
808 808 else:
809 809 # Applied but not rebased, not sure this should happen
810 810 skippedpatches.add(p.name)
811 811
812 812 if mqrebase:
813 813 mq.finish(repo, mqrebase.keys())
814 814
815 815 # We must start import from the newest revision
816 816 for rev in sorted(mqrebase, reverse=True):
817 817 if rev not in skipped:
818 818 name, isgit = mqrebase[rev]
819 819 repo.ui.note(_('updating mq patch %s to %s:%s\n') %
820 820 (name, state[rev], repo[state[rev]]))
821 821 mq.qimport(repo, (), patchname=name, git=isgit,
822 822 rev=[str(state[rev])])
823 823 else:
824 824 # Rebased and skipped
825 825 skippedpatches.add(mqrebase[rev][0])
826 826
827 827 # Patches were either applied and rebased and imported in
828 828 # order, applied and removed or unapplied. Discard the removed
829 829 # ones while preserving the original series order and guards.
830 830 newseries = [s for s in original_series
831 831 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
832 832 mq.fullseries[:] = newseries
833 833 mq.seriesdirty = True
834 834 mq.savedirty()
835 835
836 836 def updatebookmarks(repo, targetnode, nstate, originalbookmarks, tr):
837 837 'Move bookmarks to their correct changesets, and delete divergent ones'
838 838 marks = repo._bookmarks
839 839 for k, v in originalbookmarks.iteritems():
840 840 if v in nstate:
841 841 # update the bookmarks for revs that have moved
842 842 marks[k] = nstate[v]
843 843 bookmarks.deletedivergent(repo, [targetnode], k)
844 844 marks.recordchange(tr)
845 845
846 846 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
847 847 external, activebookmark):
848 848 'Store the current status to allow recovery'
849 849 f = repo.vfs("rebasestate", "w")
850 850 f.write(repo[originalwd].hex() + '\n')
851 851 f.write(repo[target].hex() + '\n')
852 852 f.write(repo[external].hex() + '\n')
853 853 f.write('%d\n' % int(collapse))
854 854 f.write('%d\n' % int(keep))
855 855 f.write('%d\n' % int(keepbranches))
856 856 f.write('%s\n' % (activebookmark or ''))
857 857 for d, v in state.iteritems():
858 858 oldrev = repo[d].hex()
859 859 if v >= 0:
860 860 newrev = repo[v].hex()
861 861 elif v == revtodo:
862 862 # To maintain format compatibility, we have to use nullid.
863 863 # Please do remove this special case when upgrading the format.
864 864 newrev = hex(nullid)
865 865 else:
866 866 newrev = v
867 867 f.write("%s:%s\n" % (oldrev, newrev))
868 868 f.close()
869 869 repo.ui.debug('rebase status stored\n')
870 870
871 871 def clearstatus(repo):
872 872 'Remove the status files'
873 873 _clearrebasesetvisibiliy(repo)
874 874 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
875 875
876 876 def restorestatus(repo):
877 877 'Restore a previously stored status'
878 878 keepbranches = None
879 879 target = None
880 880 collapse = False
881 881 external = nullrev
882 882 activebookmark = None
883 883 state = {}
884 884
885 885 try:
886 886 f = repo.vfs("rebasestate")
887 887 for i, l in enumerate(f.read().splitlines()):
888 888 if i == 0:
889 889 originalwd = repo[l].rev()
890 890 elif i == 1:
891 891 target = repo[l].rev()
892 892 elif i == 2:
893 893 external = repo[l].rev()
894 894 elif i == 3:
895 895 collapse = bool(int(l))
896 896 elif i == 4:
897 897 keep = bool(int(l))
898 898 elif i == 5:
899 899 keepbranches = bool(int(l))
900 900 elif i == 6 and not (len(l) == 81 and ':' in l):
901 901 # line 6 is a recent addition, so for backwards compatibility
902 902 # check that the line doesn't look like the oldrev:newrev lines
903 903 activebookmark = l
904 904 else:
905 905 oldrev, newrev = l.split(':')
906 906 if newrev in (str(nullmerge), str(revignored),
907 907 str(revprecursor), str(revpruned)):
908 908 state[repo[oldrev].rev()] = int(newrev)
909 909 elif newrev == nullid:
910 910 state[repo[oldrev].rev()] = revtodo
911 911 # Legacy compat special case
912 912 else:
913 913 state[repo[oldrev].rev()] = repo[newrev].rev()
914 914
915 915 except IOError as err:
916 916 if err.errno != errno.ENOENT:
917 917 raise
918 918 raise error.Abort(_('no rebase in progress'))
919 919
920 920 if keepbranches is None:
921 921 raise error.Abort(_('.hg/rebasestate is incomplete'))
922 922
923 923 skipped = set()
924 924 # recompute the set of skipped revs
925 925 if not collapse:
926 926 seen = set([target])
927 927 for old, new in sorted(state.items()):
928 928 if new != revtodo and new in seen:
929 929 skipped.add(old)
930 930 seen.add(new)
931 931 repo.ui.debug('computed skipped revs: %s\n' %
932 932 (' '.join(str(r) for r in sorted(skipped)) or None))
933 933 repo.ui.debug('rebase status resumed\n')
934 934 _setrebasesetvisibility(repo, state.keys())
935 935 return (originalwd, target, state, skipped,
936 936 collapse, keep, keepbranches, external, activebookmark)
937 937
938 938 def needupdate(repo, state):
939 939 '''check whether we should `update --clean` away from a merge, or if
940 940 somehow the working dir got forcibly updated, e.g. by older hg'''
941 941 parents = [p.rev() for p in repo[None].parents()]
942 942
943 943 # Are we in a merge state at all?
944 944 if len(parents) < 2:
945 945 return False
946 946
947 947 # We should be standing on the first as-of-yet unrebased commit.
948 948 firstunrebased = min([old for old, new in state.iteritems()
949 949 if new == nullrev])
950 950 if firstunrebased in parents:
951 951 return True
952 952
953 953 return False
954 954
955 955 def abort(repo, originalwd, target, state, activebookmark=None):
956 956 '''Restore the repository to its original state. Additional args:
957 957
958 958 activebookmark: the name of the bookmark that should be active after the
959 959 restore'''
960 960
961 961 try:
962 962 # If the first commits in the rebased set get skipped during the rebase,
963 963 # their values within the state mapping will be the target rev id. The
964 964 # dstates list must must not contain the target rev (issue4896)
965 965 dstates = [s for s in state.values() if s >= 0 and s != target]
966 966 immutable = [d for d in dstates if not repo[d].mutable()]
967 967 cleanup = True
968 968 if immutable:
969 969 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
970 970 % ', '.join(str(repo[r]) for r in immutable),
971 971 hint=_('see "hg help phases" for details'))
972 972 cleanup = False
973 973
974 974 descendants = set()
975 975 if dstates:
976 976 descendants = set(repo.changelog.descendants(dstates))
977 977 if descendants - set(dstates):
978 978 repo.ui.warn(_("warning: new changesets detected on target branch, "
979 979 "can't strip\n"))
980 980 cleanup = False
981 981
982 982 if cleanup:
983 983 shouldupdate = False
984 984 rebased = filter(lambda x: x >= 0 and x != target, state.values())
985 985 if rebased:
986 986 strippoints = [
987 987 c.node() for c in repo.set('roots(%ld)', rebased)]
988 988 shouldupdate = len([
989 989 c.node() for c in repo.set('. & (%ld)', rebased)]) > 0
990 990
991 991 # Update away from the rebase if necessary
992 992 if shouldupdate or needupdate(repo, state):
993 993 merge.update(repo, originalwd, False, True)
994 994
995 995 # Strip from the first rebased revision
996 996 if rebased:
997 997 # no backup of rebased cset versions needed
998 998 repair.strip(repo.ui, repo, strippoints)
999 999
1000 1000 if activebookmark and activebookmark in repo._bookmarks:
1001 1001 bookmarks.activate(repo, activebookmark)
1002 1002
1003 1003 finally:
1004 1004 clearstatus(repo)
1005 1005 repo.ui.warn(_('rebase aborted\n'))
1006 1006 return 0
1007 1007
1008 1008 def buildstate(repo, dest, rebaseset, collapse, obsoletenotrebased):
1009 1009 '''Define which revisions are going to be rebased and where
1010 1010
1011 1011 repo: repo
1012 1012 dest: context
1013 1013 rebaseset: set of rev
1014 1014 '''
1015 1015 _setrebasesetvisibility(repo, rebaseset)
1016 1016
1017 1017 # This check isn't strictly necessary, since mq detects commits over an
1018 1018 # applied patch. But it prevents messing up the working directory when
1019 1019 # a partially completed rebase is blocked by mq.
1020 1020 if 'qtip' in repo.tags() and (dest.node() in
1021 1021 [s.node for s in repo.mq.applied]):
1022 1022 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1023 1023
1024 1024 roots = list(repo.set('roots(%ld)', rebaseset))
1025 1025 if not roots:
1026 1026 raise error.Abort(_('no matching revisions'))
1027 1027 roots.sort()
1028 1028 state = {}
1029 1029 detachset = set()
1030 1030 for root in roots:
1031 1031 commonbase = root.ancestor(dest)
1032 1032 if commonbase == root:
1033 1033 raise error.Abort(_('source is ancestor of destination'))
1034 1034 if commonbase == dest:
1035 1035 samebranch = root.branch() == dest.branch()
1036 1036 if not collapse and samebranch and root in dest.children():
1037 1037 repo.ui.debug('source is a child of destination\n')
1038 1038 return None
1039 1039
1040 1040 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, root))
1041 1041 state.update(dict.fromkeys(rebaseset, revtodo))
1042 1042 # Rebase tries to turn <dest> into a parent of <root> while
1043 1043 # preserving the number of parents of rebased changesets:
1044 1044 #
1045 1045 # - A changeset with a single parent will always be rebased as a
1046 1046 # changeset with a single parent.
1047 1047 #
1048 1048 # - A merge will be rebased as merge unless its parents are both
1049 1049 # ancestors of <dest> or are themselves in the rebased set and
1050 1050 # pruned while rebased.
1051 1051 #
1052 1052 # If one parent of <root> is an ancestor of <dest>, the rebased
1053 1053 # version of this parent will be <dest>. This is always true with
1054 1054 # --base option.
1055 1055 #
1056 1056 # Otherwise, we need to *replace* the original parents with
1057 1057 # <dest>. This "detaches" the rebased set from its former location
1058 1058 # and rebases it onto <dest>. Changes introduced by ancestors of
1059 1059 # <root> not common with <dest> (the detachset, marked as
1060 1060 # nullmerge) are "removed" from the rebased changesets.
1061 1061 #
1062 1062 # - If <root> has a single parent, set it to <dest>.
1063 1063 #
1064 1064 # - If <root> is a merge, we cannot decide which parent to
1065 1065 # replace, the rebase operation is not clearly defined.
1066 1066 #
1067 1067 # The table below sums up this behavior:
1068 1068 #
1069 1069 # +------------------+----------------------+-------------------------+
1070 1070 # | | one parent | merge |
1071 1071 # +------------------+----------------------+-------------------------+
1072 1072 # | parent in | new parent is <dest> | parents in ::<dest> are |
1073 1073 # | ::<dest> | | remapped to <dest> |
1074 1074 # +------------------+----------------------+-------------------------+
1075 1075 # | unrelated source | new parent is <dest> | ambiguous, abort |
1076 1076 # +------------------+----------------------+-------------------------+
1077 1077 #
1078 1078 # The actual abort is handled by `defineparents`
1079 1079 if len(root.parents()) <= 1:
1080 1080 # ancestors of <root> not ancestors of <dest>
1081 1081 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
1082 1082 [root.rev()]))
1083 1083 for r in detachset:
1084 1084 if r not in state:
1085 1085 state[r] = nullmerge
1086 1086 if len(roots) > 1:
1087 1087 # If we have multiple roots, we may have "hole" in the rebase set.
1088 1088 # Rebase roots that descend from those "hole" should not be detached as
1089 1089 # other root are. We use the special `revignored` to inform rebase that
1090 1090 # the revision should be ignored but that `defineparents` should search
1091 1091 # a rebase destination that make sense regarding rebased topology.
1092 1092 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
1093 1093 for ignored in set(rebasedomain) - set(rebaseset):
1094 1094 state[ignored] = revignored
1095 1095 for r in obsoletenotrebased:
1096 1096 if obsoletenotrebased[r] is None:
1097 1097 state[r] = revpruned
1098 1098 else:
1099 1099 state[r] = revprecursor
1100 1100 return repo['.'].rev(), dest.rev(), state
1101 1101
1102 1102 def clearrebased(ui, repo, state, skipped, collapsedas=None):
1103 1103 """dispose of rebased revision at the end of the rebase
1104 1104
1105 1105 If `collapsedas` is not None, the rebase was a collapse whose result if the
1106 1106 `collapsedas` node."""
1107 1107 if obsolete.isenabled(repo, obsolete.createmarkersopt):
1108 1108 markers = []
1109 1109 for rev, newrev in sorted(state.items()):
1110 1110 if newrev >= 0:
1111 1111 if rev in skipped:
1112 1112 succs = ()
1113 1113 elif collapsedas is not None:
1114 1114 succs = (repo[collapsedas],)
1115 1115 else:
1116 1116 succs = (repo[newrev],)
1117 1117 markers.append((repo[rev], succs))
1118 1118 if markers:
1119 1119 obsolete.createmarkers(repo, markers)
1120 1120 else:
1121 1121 rebased = [rev for rev in state if state[rev] > nullmerge]
1122 1122 if rebased:
1123 1123 stripped = []
1124 1124 for root in repo.set('roots(%ld)', rebased):
1125 1125 if set(repo.changelog.descendants([root.rev()])) - set(state):
1126 1126 ui.warn(_("warning: new changesets detected "
1127 1127 "on source branch, not stripping\n"))
1128 1128 else:
1129 1129 stripped.append(root.node())
1130 1130 if stripped:
1131 1131 # backup the old csets by default
1132 1132 repair.strip(ui, repo, stripped, "all")
1133 1133
1134 1134
1135 1135 def pullrebase(orig, ui, repo, *args, **opts):
1136 1136 'Call rebase after pull if the latter has been invoked with --rebase'
1137 1137 ret = None
1138 1138 if opts.get('rebase'):
1139 1139 wlock = lock = None
1140 1140 try:
1141 1141 wlock = repo.wlock()
1142 1142 lock = repo.lock()
1143 1143 if opts.get('update'):
1144 1144 del opts['update']
1145 1145 ui.debug('--update and --rebase are not compatible, ignoring '
1146 1146 'the update flag\n')
1147 1147
1148 movemarkfrom = repo['.'].node()
1149 1148 revsprepull = len(repo)
1150 1149 origpostincoming = commands.postincoming
1151 1150 def _dummy(*args, **kwargs):
1152 1151 pass
1153 1152 commands.postincoming = _dummy
1154 1153 try:
1155 1154 ret = orig(ui, repo, *args, **opts)
1156 1155 finally:
1157 1156 commands.postincoming = origpostincoming
1158 1157 revspostpull = len(repo)
1159 1158 if revspostpull > revsprepull:
1160 1159 # --rev option from pull conflict with rebase own --rev
1161 1160 # dropping it
1162 1161 if 'rev' in opts:
1163 1162 del opts['rev']
1164 1163 # positional argument from pull conflicts with rebase's own
1165 1164 # --source.
1166 1165 if 'source' in opts:
1167 1166 del opts['source']
1168 1167 if rebase(ui, repo, **opts) == _nothingtorebase():
1169 branch = repo[None].branch()
1170 dest = repo[branch].rev()
1171 if dest != repo['.'].rev():
1172 # there was nothing to rebase we force an update
1173 hg.update(repo, dest)
1174 if bookmarks.update(repo, [movemarkfrom],
1175 repo['.'].node()):
1176 ui.status(_("updating bookmark %s\n")
1177 % repo._activebookmark)
1168 rev, _a, _b = destutil.destupdate(repo)
1169 if rev != repo['.'].rev(): # we could update
1170 # not passing argument to get the bare update behavior
1171 # with warning and trumpets
1172 commands.update(ui, repo)
1178 1173 finally:
1179 1174 release(lock, wlock)
1180 1175 else:
1181 1176 if opts.get('tool'):
1182 1177 raise error.Abort(_('--tool can only be used with --rebase'))
1183 1178 ret = orig(ui, repo, *args, **opts)
1184 1179
1185 1180 return ret
1186 1181
1187 1182 def _setrebasesetvisibility(repo, revs):
1188 1183 """store the currently rebased set on the repo object
1189 1184
1190 1185 This is used by another function to prevent rebased revision to because
1191 1186 hidden (see issue4505)"""
1192 1187 repo = repo.unfiltered()
1193 1188 revs = set(revs)
1194 1189 repo._rebaseset = revs
1195 1190 # invalidate cache if visibility changes
1196 1191 hiddens = repo.filteredrevcache.get('visible', set())
1197 1192 if revs & hiddens:
1198 1193 repo.invalidatevolatilesets()
1199 1194
1200 1195 def _clearrebasesetvisibiliy(repo):
1201 1196 """remove rebaseset data from the repo"""
1202 1197 repo = repo.unfiltered()
1203 1198 if '_rebaseset' in vars(repo):
1204 1199 del repo._rebaseset
1205 1200
1206 1201 def _rebasedvisible(orig, repo):
1207 1202 """ensure rebased revs stay visible (see issue4505)"""
1208 1203 blockers = orig(repo)
1209 1204 blockers.update(getattr(repo, '_rebaseset', ()))
1210 1205 return blockers
1211 1206
1212 1207 def _filterobsoleterevs(repo, revs):
1213 1208 """returns a set of the obsolete revisions in revs"""
1214 1209 return set(r for r in revs if repo[r].obsolete())
1215 1210
1216 1211 def _computeobsoletenotrebased(repo, rebaseobsrevs, dest):
1217 1212 """return a mapping obsolete => successor for all obsolete nodes to be
1218 1213 rebased that have a successors in the destination
1219 1214
1220 1215 obsolete => None entries in the mapping indicate nodes with no succesor"""
1221 1216 obsoletenotrebased = {}
1222 1217
1223 1218 # Build a mapping successor => obsolete nodes for the obsolete
1224 1219 # nodes to be rebased
1225 1220 allsuccessors = {}
1226 1221 cl = repo.changelog
1227 1222 for r in rebaseobsrevs:
1228 1223 node = cl.node(r)
1229 1224 for s in obsolete.allsuccessors(repo.obsstore, [node]):
1230 1225 try:
1231 1226 allsuccessors[cl.rev(s)] = cl.rev(node)
1232 1227 except LookupError:
1233 1228 pass
1234 1229
1235 1230 if allsuccessors:
1236 1231 # Look for successors of obsolete nodes to be rebased among
1237 1232 # the ancestors of dest
1238 1233 ancs = cl.ancestors([repo[dest].rev()],
1239 1234 stoprev=min(allsuccessors),
1240 1235 inclusive=True)
1241 1236 for s in allsuccessors:
1242 1237 if s in ancs:
1243 1238 obsoletenotrebased[allsuccessors[s]] = s
1244 1239 elif (s == allsuccessors[s] and
1245 1240 allsuccessors.values().count(s) == 1):
1246 1241 # plain prune
1247 1242 obsoletenotrebased[s] = None
1248 1243
1249 1244 return obsoletenotrebased
1250 1245
1251 1246 def summaryhook(ui, repo):
1252 1247 if not os.path.exists(repo.join('rebasestate')):
1253 1248 return
1254 1249 try:
1255 1250 state = restorestatus(repo)[2]
1256 1251 except error.RepoLookupError:
1257 1252 # i18n: column positioning for "hg summary"
1258 1253 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1259 1254 ui.write(msg)
1260 1255 return
1261 1256 numrebased = len([i for i in state.itervalues() if i >= 0])
1262 1257 # i18n: column positioning for "hg summary"
1263 1258 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1264 1259 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1265 1260 ui.label(_('%d remaining'), 'rebase.remaining') %
1266 1261 (len(state) - numrebased)))
1267 1262
1268 1263 def uisetup(ui):
1269 1264 #Replace pull with a decorator to provide --rebase option
1270 1265 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1271 1266 entry[1].append(('', 'rebase', None,
1272 1267 _("rebase working directory to branch head")))
1273 1268 entry[1].append(('t', 'tool', '',
1274 1269 _("specify merge tool for rebase")))
1275 1270 cmdutil.summaryhooks.add('rebase', summaryhook)
1276 1271 cmdutil.unfinishedstates.append(
1277 1272 ['rebasestate', False, False, _('rebase in progress'),
1278 1273 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
1279 1274 cmdutil.afterresolvedstates.append(
1280 1275 ['rebasestate', _('hg rebase --continue')])
1281 1276 # ensure rebased rev are not hidden
1282 1277 extensions.wrapfunction(repoview, '_getdynamicblockers', _rebasedvisible)
1283 1278 revsetpredicate.setup()
@@ -1,268 +1,311 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > rebase=
4 4 >
5 5 > [alias]
6 6 > tglog = log -G --template "{rev}: '{desc}' {branches}\n"
7 7 > EOF
8 8
9 9
10 10 $ hg init a
11 11 $ cd a
12 12
13 13 $ echo C1 > C1
14 14 $ hg ci -Am C1
15 15 adding C1
16 16
17 17 $ echo C2 > C2
18 18 $ hg ci -Am C2
19 19 adding C2
20 20
21 21 $ cd ..
22 22
23 23 $ hg clone a b
24 24 updating to branch default
25 25 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
26 26
27 27 $ hg clone a c
28 28 updating to branch default
29 29 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
30 30
31 31 $ cd b
32 32
33 33 $ echo L1 > L1
34 34 $ hg ci -Am L1
35 35 adding L1
36 36
37 37
38 38 $ cd ../a
39 39
40 40 $ echo R1 > R1
41 41 $ hg ci -Am R1
42 42 adding R1
43 43
44 44
45 45 $ cd ../b
46 46
47 47 Now b has one revision to be pulled from a:
48 48
49 49 $ hg pull --rebase
50 50 pulling from $TESTTMP/a (glob)
51 51 searching for changes
52 52 adding changesets
53 53 adding manifests
54 54 adding file changes
55 55 added 1 changesets with 1 changes to 1 files (+1 heads)
56 56 rebasing 2:ff8d69a621f9 "L1"
57 57 saved backup bundle to $TESTTMP/b/.hg/strip-backup/ff8d69a621f9-160fa373-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 - working directory parent is already an ancestor of destination 77ae9631bcca
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 117
118 118 pull --rebase works when a specific revision is pulled (issue3619)
119 119
120 120 $ cd a
121 121 $ hg tglog
122 122 @ 2: 'R1'
123 123 |
124 124 o 1: 'C2'
125 125 |
126 126 o 0: 'C1'
127 127
128 128 $ echo R2 > R2
129 129 $ hg ci -Am R2
130 130 adding R2
131 131 $ echo R3 > R3
132 132 $ hg ci -Am R3
133 133 adding R3
134 134 $ cd ../c
135 135 $ hg tglog
136 136 o 2: 'R1'
137 137 |
138 138 @ 1: 'C2'
139 139 |
140 140 o 0: 'C1'
141 141
142 142 $ echo L1 > L1
143 143 $ hg ci -Am L1
144 144 adding L1
145 145 created new head
146 146 $ hg pull --rev tip --rebase
147 147 pulling from $TESTTMP/a (glob)
148 148 searching for changes
149 149 adding changesets
150 150 adding manifests
151 151 adding file changes
152 152 added 2 changesets with 2 changes to 2 files
153 153 rebasing 3:ff8d69a621f9 "L1"
154 154 saved backup bundle to $TESTTMP/c/.hg/strip-backup/ff8d69a621f9-160fa373-backup.hg (glob)
155 155 $ hg tglog
156 156 @ 5: 'L1'
157 157 |
158 158 o 4: 'R3'
159 159 |
160 160 o 3: 'R2'
161 161 |
162 162 o 2: 'R1'
163 163 |
164 164 o 1: 'C2'
165 165 |
166 166 o 0: 'C1'
167 167
168 168 pull --rebase works with bundle2 turned on
169 169
170 170 $ cd ../a
171 171 $ echo R4 > R4
172 172 $ hg ci -Am R4
173 173 adding R4
174 174 $ hg tglog
175 175 @ 5: 'R4'
176 176 |
177 177 o 4: 'R3'
178 178 |
179 179 o 3: 'R2'
180 180 |
181 181 o 2: 'R1'
182 182 |
183 183 o 1: 'C2'
184 184 |
185 185 o 0: 'C1'
186 186
187 187 $ cd ../c
188 188 $ hg pull --rebase
189 189 pulling from $TESTTMP/a (glob)
190 190 searching for changes
191 191 adding changesets
192 192 adding manifests
193 193 adding file changes
194 194 added 1 changesets with 1 changes to 1 files (+1 heads)
195 195 rebasing 5:518d153c0ba3 "L1"
196 196 saved backup bundle to $TESTTMP/c/.hg/strip-backup/518d153c0ba3-73407f14-backup.hg (glob)
197 197 $ hg tglog
198 198 @ 6: 'L1'
199 199 |
200 200 o 5: 'R4'
201 201 |
202 202 o 4: 'R3'
203 203 |
204 204 o 3: 'R2'
205 205 |
206 206 o 2: 'R1'
207 207 |
208 208 o 1: 'C2'
209 209 |
210 210 o 0: 'C1'
211 211
212 212
213 213 pull --rebase only update if there is nothing to rebase
214 214
215 215 $ cd ../a
216 216 $ echo R5 > R5
217 217 $ hg ci -Am R5
218 218 adding R5
219 219 $ hg tglog
220 220 @ 6: 'R5'
221 221 |
222 222 o 5: 'R4'
223 223 |
224 224 o 4: 'R3'
225 225 |
226 226 o 3: 'R2'
227 227 |
228 228 o 2: 'R1'
229 229 |
230 230 o 1: 'C2'
231 231 |
232 232 o 0: 'C1'
233 233
234 234 $ cd ../c
235 235 $ echo L2 > L2
236 236 $ hg ci -Am L2
237 237 adding L2
238 238 $ hg up 'desc(L1)'
239 239 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
240 240 $ hg pull --rebase
241 241 pulling from $TESTTMP/a (glob)
242 242 searching for changes
243 243 adding changesets
244 244 adding manifests
245 245 adding file changes
246 246 added 1 changesets with 1 changes to 1 files (+1 heads)
247 247 rebasing 6:0d0727eb7ce0 "L1"
248 248 rebasing 7:c1f58876e3bf "L2"
249 249 saved backup bundle to $TESTTMP/c/.hg/strip-backup/0d0727eb7ce0-ef61ccb2-backup.hg (glob)
250 250 $ hg tglog
251 251 o 8: 'L2'
252 252 |
253 253 @ 7: 'L1'
254 254 |
255 255 o 6: 'R5'
256 256 |
257 257 o 5: 'R4'
258 258 |
259 259 o 4: 'R3'
260 260 |
261 261 o 3: 'R2'
262 262 |
263 263 o 2: 'R1'
264 264 |
265 265 o 1: 'C2'
266 266 |
267 267 o 0: 'C1'
268 268
269
270 pull --rebase update (no rebase) use proper update:
271
272 - warn about other head.
273
274 $ cd ../a
275 $ echo R6 > R6
276 $ hg ci -Am R6
277 adding R6
278 $ cd ../c
279 $ hg up 'desc(R5)'
280 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
281 $ hg pull --rebase
282 pulling from $TESTTMP/a (glob)
283 searching for changes
284 adding changesets
285 adding manifests
286 adding file changes
287 added 1 changesets with 1 changes to 1 files (+1 heads)
288 nothing to rebase - working directory parent is already an ancestor of destination 65bc164c1d9b
289 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
290 1 other heads for branch "default"
291 $ hg tglog
292 @ 9: 'R6'
293 |
294 | o 8: 'L2'
295 | |
296 | o 7: 'L1'
297 |/
298 o 6: 'R5'
299 |
300 o 5: 'R4'
301 |
302 o 4: 'R3'
303 |
304 o 3: 'R2'
305 |
306 o 2: 'R1'
307 |
308 o 1: 'C2'
309 |
310 o 0: 'C1'
311
General Comments 0
You need to be logged in to leave comments. Login now