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