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