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