hgext/evolve.py
changeset 579 f6063ef211fd
parent 577 2cd2ee20d9fa
child 589 8945a62f9096
equal deleted inserted replaced
578:02f34904305f 579:f6063ef211fd
   311                       marker[2],
   311                       marker[2],
   312                       marker[3])
   312                       marker[3])
   313         yield marker
   313         yield marker
   314 
   314 
   315 
   315 
   316 #####################################################################
   316 cachefuncs = obsolete.cachefuncs
   317 ### Obsolescence Caching Logic                                    ###
   317 cachefor = obsolete.cachefor
   318 #####################################################################
   318 getobscache = obsolete.getobscache
   319 
   319 clearobscaches = obsolete.clearobscaches
   320 # IN CORE fb72eec7efd8
       
   321 
       
   322 # Obsolescence related logic can be very slow if we don't have efficient cache.
       
   323 #
       
   324 # This section implements a cache mechanism that did not make it into core for
       
   325 # time reason. It stores meaningful set of revisions related to obsolescence
       
   326 # (obsolete, unstable, etc.)
       
   327 #
       
   328 # Here is:
       
   329 #
       
   330 # - Computation of meaningful sets
       
   331 # - Cache access logic,
       
   332 # - Cache invalidation logic,
       
   333 # - revset and ctx using this cache.
       
   334 #
       
   335 
       
   336 
       
   337 ### Computation of meaningful set
       
   338 #
       
   339 # Most set can be computed with "simple" revset.
       
   340 
       
   341 #: { set name -> function to compute this set } mapping
       
   342 #:   function take a single "repo" argument.
       
   343 #:
       
   344 #: Use the `cachefor` decorator to register new cache function
       
   345 try:
       
   346     cachefuncs = obsolete.cachefuncs
       
   347     cachefor = obsolete.cachefor
       
   348     getobscache = obsolete.getobscache
       
   349     clearobscaches = obsolete.clearobscaches
       
   350 except AttributeError:
       
   351     cachefuncs = {}
       
   352 
       
   353     def cachefor(name):
       
   354         """Decorator to register a function as computing the cache for a set"""
       
   355         def decorator(func):
       
   356             assert name not in cachefuncs
       
   357             cachefuncs[name] = func
       
   358             return func
       
   359         return decorator
       
   360 
       
   361     @cachefor('obsolete')
       
   362     def _computeobsoleteset(repo):
       
   363         """the set of obsolete revisions"""
       
   364         obs = set()
       
   365         nm = repo.changelog.nodemap
       
   366         for prec in repo.obsstore.precursors:
       
   367             rev = nm.get(prec)
       
   368             if rev is not None:
       
   369                 obs.add(rev)
       
   370         return set(repo.revs('%ld - public()', obs))
       
   371 
       
   372     @cachefor('unstable')
       
   373     def _computeunstableset(repo):
       
   374         """the set of non obsolete revisions with obsolete parents"""
       
   375         return set(repo.revs('(obsolete()::) - obsolete()'))
       
   376 
       
   377     @cachefor('suspended')
       
   378     def _computesuspendedset(repo):
       
   379         """the set of obsolete parents with non obsolete descendants"""
       
   380         return set(repo.revs('obsolete() and obsolete()::unstable()'))
       
   381 
       
   382     @cachefor('extinct')
       
   383     def _computeextinctset(repo):
       
   384         """the set of obsolete parents without non obsolete descendants"""
       
   385         return set(repo.revs('obsolete() - obsolete()::unstable()'))
       
   386 
       
   387     @eh.wrapfunction(obsolete.obsstore, '__init__')
       
   388     def _initobsstorecache(orig, obsstore, *args, **kwargs):
       
   389         """add a cache attribute to obsstore"""
       
   390         obsstore.caches = {}
       
   391         return orig(obsstore, *args, **kwargs)
       
   392 
       
   393 ### Cache access
       
   394 
       
   395     def getobscache(repo, name):
       
   396         """Return the set of revision that belong to the <name> set
       
   397 
       
   398         Such access may compute the set and cache it for future use"""
       
   399         if not repo.obsstore:
       
   400             return ()
       
   401         if name not in repo.obsstore.caches:
       
   402             repo.obsstore.caches[name] = cachefuncs[name](repo)
       
   403         return repo.obsstore.caches[name]
       
   404 
       
   405 ### Cache clean up
       
   406 #
       
   407 # To be simple we need to invalidate obsolescence cache when:
       
   408 #
       
   409 # - new changeset is added:
       
   410 # - public phase is changed
       
   411 # - obsolescence marker are added
       
   412 # - strip is used a repo
       
   413 
       
   414 
       
   415     def clearobscaches(repo):
       
   416         """Remove all obsolescence related cache from a repo
       
   417 
       
   418         This remove all cache in obsstore is the obsstore already exist on the
       
   419         repo.
       
   420 
       
   421         (We could be smarter here)"""
       
   422         if 'obsstore' in repo._filecache:
       
   423             repo.obsstore.caches.clear()
       
   424 
       
   425     @eh.wrapfunction(localrepo.localrepository, 'addchangegroup')  # new changeset
       
   426     @eh.wrapfunction(phases, 'retractboundary')  # phase movement
       
   427     @eh.wrapfunction(phases, 'advanceboundary')  # phase movement
       
   428     @eh.wrapfunction(localrepo.localrepository, 'destroyed')  # strip
       
   429     def wrapclearcache(orig, repo, *args, **kwargs):
       
   430         try:
       
   431             return orig(repo, *args, **kwargs)
       
   432         finally:
       
   433             # we are a bit wide here
       
   434             # we could restrict to:
       
   435             # advanceboundary + phase==public
       
   436             # retractboundary + phase==draft
       
   437             clearobscaches(repo)
       
   438 
       
   439     @eh.wrapfunction(obsolete.obsstore, 'add')  # new marker
       
   440     def clearonadd(orig, obsstore, *args, **kwargs):
       
   441         try:
       
   442             return orig(obsstore, *args, **kwargs)
       
   443         finally:
       
   444             obsstore.caches.clear()
       
   445 
       
   446 ### Use the case
       
   447 # Function in core that could benefic from the cache are overwritten by cache using version
       
   448 
       
   449 # changectx method
       
   450 
       
   451     @eh.addattr(context.changectx, 'unstable')
       
   452     def unstable(ctx):
       
   453         """is the changeset unstable (have obsolete ancestor)"""
       
   454         if ctx.node() is None:
       
   455             return False
       
   456         return ctx.rev() in getobscache(ctx._repo, 'unstable')
       
   457 
       
   458 
       
   459     @eh.addattr(context.changectx, 'extinct')
       
   460     def extinct(ctx):
       
   461         """is the changeset extinct by other"""
       
   462         if ctx.node() is None:
       
   463             return False
       
   464         return ctx.rev() in getobscache(ctx._repo, 'extinct')
       
   465 
       
   466 # revset
       
   467 
       
   468     @eh.revset('obsolete')
       
   469     def revsetobsolete(repo, subset, x):
       
   470         """``obsolete()``
       
   471         Changeset is obsolete.
       
   472         """
       
   473         args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
       
   474         obsoletes = getobscache(repo, 'obsolete')
       
   475         return [r for r in subset if r in obsoletes]
       
   476 
       
   477     @eh.revset('unstable')
       
   478     def revsetunstable(repo, subset, x):
       
   479         """``unstable()``
       
   480         Unstable changesets are non-obsolete with obsolete ancestors.
       
   481         """
       
   482         args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
       
   483         unstables = getobscache(repo, 'unstable')
       
   484         return [r for r in subset if r in unstables]
       
   485 
       
   486     @eh.revset('extinct')
       
   487     def revsetextinct(repo, subset, x):
       
   488         """``extinct()``
       
   489         Obsolete changesets with obsolete descendants only.
       
   490         """
       
   491         args = revset.getargs(x, 0, 0, 'extinct takes no arguments')
       
   492         extincts = getobscache(repo, 'extinct')
       
   493         return [r for r in subset if r in extincts]
       
   494 
   320 
   495 #####################################################################
   321 #####################################################################
   496 ### Complete troubles computation logic                           ###
   322 ### Complete troubles computation logic                           ###
   497 #####################################################################
   323 #####################################################################
   498 
   324 
   648 # - useful alias pstatus and pdiff (should probably go in evolve)
   474 # - useful alias pstatus and pdiff (should probably go in evolve)
   649 # - "troubles" method on changectx
   475 # - "troubles" method on changectx
   650 # - function to travel throught the obsolescence graph
   476 # - function to travel throught the obsolescence graph
   651 # - function to find useful changeset to stabilize
   477 # - function to find useful changeset to stabilize
   652 
   478 
   653 ### Marker Create
   479 createmarkers = obsolete.createmarkers
   654 # NOW IN CORE f85816af6294
       
   655 try:
       
   656     createmarkers = obsolete.createmarkers
       
   657 except AttributeError:
       
   658     def createmarkers(repo, relations, metadata=None, flag=0):
       
   659         """Add obsolete markers between changeset in a repo
       
   660 
       
   661         <relations> must be an iterable of (<old>, (<new>, ...)) tuple.
       
   662         `old` and `news` are changectx.
       
   663 
       
   664         Current user and date are used except if specified otherwise in the
       
   665         metadata attribute.
       
   666 
       
   667         /!\ assume the repo have been locked by the user /!\
       
   668         """
       
   669         # prepare metadata
       
   670         if metadata is None:
       
   671             metadata = {}
       
   672         if 'date' not in metadata:
       
   673             metadata['date'] = '%i %i' % util.makedate()
       
   674         if 'user' not in metadata:
       
   675             metadata['user'] = repo.ui.username()
       
   676         # check future marker
       
   677         tr = repo.transaction('add-obsolescence-marker')
       
   678         try:
       
   679             for prec, sucs in relations:
       
   680                 if not prec.mutable():
       
   681                     raise util.Abort("cannot obsolete immutable changeset: %s" % prec)
       
   682                 nprec = prec.node()
       
   683                 nsucs = tuple(s.node() for s in sucs)
       
   684                 if nprec in nsucs:
       
   685                     raise util.Abort("changeset %s cannot obsolete himself" % prec)
       
   686                 repo.obsstore.create(tr, nprec, nsucs, flag, metadata)
       
   687                 clearobscaches(repo)
       
   688             tr.close()
       
   689         finally:
       
   690             tr.release()
       
   691 
   480 
   692 
   481 
   693 ### Useful alias
   482 ### Useful alias
   694 
   483 
   695 @eh.uisetup
   484 @eh.uisetup
   838 #####################################################################
   627 #####################################################################
   839 
   628 
   840 # this section add several useful revset symbol not yet in core.
   629 # this section add several useful revset symbol not yet in core.
   841 # they are subject to changes
   630 # they are subject to changes
   842 
   631 
   843 
       
   844 if 'hidden' not in revset.symbols:
       
   845     # in 2.3+
       
   846     @eh.revset('hidden')
       
   847     def revsethidden(repo, subset, x):
       
   848         """``hidden()``
       
   849         Changeset is hidden.
       
   850         """
       
   851         args = revset.getargs(x, 0, 0, 'hidden takes no argument')
       
   852         return [r for r in subset if r in repo.hiddenrevs]
       
   853 
   632 
   854 ### XXX I'm not sure this revset is useful
   633 ### XXX I'm not sure this revset is useful
   855 @eh.revset('suspended')
   634 @eh.revset('suspended')
   856 def revsetsuspended(repo, subset, x):
   635 def revsetsuspended(repo, subset, x):
   857     """``suspended()``
   636     """``suspended()``
  1006 
   785 
  1007 #####################################################################
   786 #####################################################################
  1008 ### Core Other extension compat                                   ###
   787 ### Core Other extension compat                                   ###
  1009 #####################################################################
   788 #####################################################################
  1010 
   789 
  1011 # This section make official history rewritter create obsolete marker
       
  1012 
       
  1013 
       
  1014 ### commit --amend
       
  1015 # make commit --amend create obsolete marker
       
  1016 #
       
  1017 # The precursor is still strip from the repository.
       
  1018 
       
  1019 # IN CORE 63e45aee46d4
       
  1020 
       
  1021 if getattr(cmdutil, 'obsolete', None) is None:
       
  1022     @eh.wrapfunction(cmdutil, 'amend')
       
  1023     def wrapcmdutilamend(orig, ui, repo, commitfunc, old, *args, **kwargs):
       
  1024         oldnode = old.node()
       
  1025         new = orig(ui, repo, commitfunc, old, *args, **kwargs)
       
  1026         if new != oldnode:
       
  1027             lock = repo.lock()
       
  1028             try:
       
  1029                 tr = repo.transaction('post-amend-obst')
       
  1030                 try:
       
  1031                     meta = {
       
  1032                         'date':  '%i %i' % util.makedate(),
       
  1033                         'user': ui.username(),
       
  1034                         }
       
  1035                     repo.obsstore.create(tr, oldnode, [new], 0, meta)
       
  1036                     tr.close()
       
  1037                     clearobscaches(repo)
       
  1038                 finally:
       
  1039                     tr.release()
       
  1040             finally:
       
  1041                 lock.release()
       
  1042         return new
       
  1043 
       
  1044 ### rebase
       
  1045 #
       
  1046 # - ignore obsolete changeset
       
  1047 # - create obsolete marker *instead of* striping
       
  1048 
       
  1049 def buildstate(orig, repo, dest, rebaseset, *ags, **kws):
       
  1050     """wrapper for rebase 's buildstate that exclude obsolete changeset"""
       
  1051 
       
  1052     rebaseset = repo.revs('%ld - extinct()', rebaseset)
       
  1053     if not rebaseset:
       
  1054         repo.ui.warn(_('whole rebase set is extinct and ignored.\n'))
       
  1055         return {}
       
  1056     root = min(rebaseset)
       
  1057     if (not getattr(repo, '_rebasekeep', False)
       
  1058         and not repo[root].mutable()):
       
  1059         raise util.Abort(_("can't rebase immutable changeset %s") % repo[root],
       
  1060                          hint=_('see hg help phases for details'))
       
  1061     return orig(repo, dest, rebaseset, *ags, **kws)
       
  1062 
       
  1063 def defineparents(orig, repo, rev, target, state, *args, **kwargs):
       
  1064     rebasestate = getattr(repo, '_rebasestate', None)
       
  1065     if rebasestate is not None:
       
  1066         repo._rebasestate = dict(state)
       
  1067         repo._rebasetarget = target
       
  1068     return orig(repo, rev, target, state, *args, **kwargs)
       
  1069 
       
  1070 def concludenode(orig, repo, rev, p1, *args, **kwargs):
       
  1071     """wrapper for rebase 's concludenode that set obsolete relation"""
       
  1072     newrev = orig(repo, rev, p1, *args, **kwargs)
       
  1073     rebasestate = getattr(repo, '_rebasestate', None)
       
  1074     if rebasestate is not None:
       
  1075         if newrev is not None:
       
  1076             nrev = repo[newrev].rev()
       
  1077         else:
       
  1078             nrev = p1
       
  1079         repo._rebasestate[rev] = nrev
       
  1080     return newrev
       
  1081 
       
  1082 def cmdrebase(orig, ui, repo, *args, **kwargs):
       
  1083 
       
  1084     reallykeep = kwargs.get('keep', False)
       
  1085     kwargs = dict(kwargs)
       
  1086     kwargs['keep'] = True
       
  1087     repo._rebasekeep = reallykeep
       
  1088 
       
  1089     # We want to mark rebased revision as obsolete and set their
       
  1090     # replacements if any. Doing it in concludenode() prevents
       
  1091     # aborting the rebase, and is not called with all relevant
       
  1092     # revisions in --collapse case. Instead, we try to track the
       
  1093     # rebase state structure by sampling/updating it in
       
  1094     # defineparents() and concludenode(). The obsolete markers are
       
  1095     # added from this state after a successful call.
       
  1096     repo._rebasestate = {}
       
  1097     repo._rebasetarget = None
       
  1098     try:
       
  1099         l = repo.lock()
       
  1100         try:
       
  1101             res = orig(ui, repo, *args, **kwargs)
       
  1102             if not reallykeep:
       
  1103                 # Filter nullmerge or unrebased entries
       
  1104                 repo._rebasestate = dict(p for p in repo._rebasestate.iteritems()
       
  1105                                          if p[1] >= 0)
       
  1106                 if not res and not kwargs.get('abort') and repo._rebasestate:
       
  1107                     # Rebased revisions are assumed to be descendants of
       
  1108                     # targetrev. If a source revision is mapped to targetrev
       
  1109                     # or to another rebased revision, it must have been
       
  1110                     # removed.
       
  1111                     markers = []
       
  1112                     if kwargs.get('collapse'):
       
  1113                         # collapse assume revision disapear because they are all
       
  1114                         # in the created revision
       
  1115                         newrevs = set(repo._rebasestate.values())
       
  1116                         newrevs.remove(repo._rebasetarget)
       
  1117                         if newrevs:
       
  1118                             # we create new revision.
       
  1119                             # A single one by --collapse design
       
  1120                             assert len(newrevs) == 1
       
  1121                             new = tuple(repo[n] for n in newrevs)
       
  1122                         else:
       
  1123                             # every body died. no new changeset created
       
  1124                             new = (repo[repo._rebasetarget],)
       
  1125                         for rev, newrev in sorted(repo._rebasestate.items()):
       
  1126                             markers.append((repo[rev], new))
       
  1127                     else:
       
  1128                         # no collapse assume revision disapear because they are
       
  1129                         # contained in parent
       
  1130                         for rev, newrev in sorted(repo._rebasestate.items()):
       
  1131                             markers.append((repo[rev], (repo[newrev],)))
       
  1132                     createmarkers(repo, markers)
       
  1133             return res
       
  1134         finally:
       
  1135             l.release()
       
  1136     finally:
       
  1137         delattr(repo, '_rebasestate')
       
  1138         delattr(repo, '_rebasetarget')
       
  1139 
   790 
  1140 @eh.extsetup
   791 @eh.extsetup
  1141 def _rebasewrapping(ui):
   792 def _rebasewrapping(ui):
  1142     # warning about more obsolete
   793     # warning about more obsolete
  1143     try:
   794     try:
  1144         rebase = extensions.find('rebase')
   795         rebase = extensions.find('rebase')
  1145         if rebase:
   796         if rebase:
  1146             incore = getattr(rebase, 'obsolete', None) is not None
       
  1147             if not incore:
       
  1148                 extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase)
       
  1149             extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors)
   797             extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors)
  1150             if not incore:
       
  1151                 extensions.wrapfunction(rebase, 'buildstate', buildstate)
       
  1152                 extensions.wrapfunction(rebase, 'defineparents', defineparents)
       
  1153                 extensions.wrapfunction(rebase, 'concludenode', concludenode)
       
  1154     except KeyError:
   798     except KeyError:
  1155         pass  # rebase not found
   799         pass  # rebase not found
  1156 
   800 
  1157 
   801 
  1158 #####################################################################
   802 #####################################################################