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