hgext/inhibit.py
branchmercurial-3.9
changeset 2110 f1ffd093ef30
parent 1816 bb665c99562a
parent 2109 90ab79764ce4
child 2111 ec04eb4d2c6e
child 2261 3e339f6717c7
equal deleted inserted replaced
1816:bb665c99562a 2110:f1ffd093ef30
     1 """reduce the changesets evolution feature scope for early and noob friendly ui
       
     2 
       
     3 the full scale changeset evolution have some massive bleeding edge and it is
       
     4 very easy for people not very intimate with the concept to end up in intricate
       
     5 situation. in order to get some of the benefit sooner, this extension is
       
     6 disabling some of the less polished aspect of evolution. it should gradually
       
     7 get thinner and thinner as changeset evolution will get more polished. this
       
     8 extension is only recommended for large scale organisations. individual user
       
     9 should probably stick on using evolution in its current state, understand its
       
    10 concept and provide feedback
       
    11 
       
    12 This extension provides the ability to "inhibit" obsolescence markers. obsolete
       
    13 revision can be cheaply brought back to life that way.
       
    14 However as the inhibitor are not fitting in an append only model, this is
       
    15 incompatible with sharing mutable history.
       
    16 """
       
    17 from mercurial import localrepo
       
    18 from mercurial import obsolete
       
    19 from mercurial import extensions
       
    20 from mercurial import cmdutil
       
    21 from mercurial import error
       
    22 from mercurial import scmutil
       
    23 from mercurial import commands
       
    24 from mercurial import lock as lockmod
       
    25 from mercurial import bookmarks
       
    26 from mercurial import util
       
    27 from mercurial.i18n import _
       
    28 
       
    29 cmdtable = {}
       
    30 command = cmdutil.command(cmdtable)
       
    31 
       
    32 def _inhibitenabled(repo):
       
    33     return util.safehasattr(repo, '_obsinhibit')
       
    34 
       
    35 def reposetup(ui, repo):
       
    36 
       
    37     class obsinhibitedrepo(repo.__class__):
       
    38 
       
    39         @localrepo.storecache('obsinhibit')
       
    40         def _obsinhibit(self):
       
    41             # XXX we should make sure it is invalidated by transaction failure
       
    42             obsinhibit = set()
       
    43             raw = self.svfs.tryread('obsinhibit')
       
    44             for i in xrange(0, len(raw), 20):
       
    45                 obsinhibit.add(raw[i:i + 20])
       
    46             return obsinhibit
       
    47 
       
    48         def commit(self, *args, **kwargs):
       
    49             newnode = super(obsinhibitedrepo, self).commit(*args, **kwargs)
       
    50             if newnode is not None:
       
    51                 _inhibitmarkers(repo, [newnode])
       
    52             return newnode
       
    53 
       
    54     repo.__class__ = obsinhibitedrepo
       
    55 
       
    56 def _update(orig, ui, repo, *args, **kwargs):
       
    57     """
       
    58     When moving to a commit we want to inhibit any obsolete commit affecting
       
    59     the changeset we are updating to. In other words we don't want any visible
       
    60     commit to be obsolete.
       
    61     """
       
    62     wlock = None
       
    63     try:
       
    64         # Evolve is running a hook on lock release to display a warning message
       
    65         # if the workind dir's parent is obsolete.
       
    66         # We take the lock here to make sure that we inhibit the parent before
       
    67         # that hook get a chance to run.
       
    68         wlock = repo.wlock()
       
    69         res = orig(ui, repo, *args, **kwargs)
       
    70         newhead = repo['.'].node()
       
    71         _inhibitmarkers(repo, [newhead])
       
    72         return res
       
    73     finally:
       
    74         lockmod.release(wlock)
       
    75 
       
    76 def _bookmarkchanged(orig, bkmstoreinst, *args, **kwargs):
       
    77     """ Add inhibition markers to every obsolete bookmarks """
       
    78     repo = bkmstoreinst._repo
       
    79     bkmstorenodes = [repo[v].node() for v in bkmstoreinst.values()]
       
    80     _inhibitmarkers(repo, bkmstorenodes)
       
    81     return orig(bkmstoreinst, *args, **kwargs)
       
    82 
       
    83 def _bookmark(orig, ui, repo, *bookmarks, **opts):
       
    84     """ Add a -D option to the bookmark command, map it to prune -B """
       
    85     haspruneopt = opts.get('prune', False)
       
    86     if not haspruneopt:
       
    87         return orig(ui, repo, *bookmarks, **opts)
       
    88     elif opts.get('rename'):
       
    89         raise error.Abort('Cannot use both -m and -D')
       
    90     elif len(bookmarks) == 0:
       
    91         hint = _('make sure to put a space between -D and your bookmark name')
       
    92         raise error.Abort(_('Error, please check your command'), hint=hint)
       
    93 
       
    94     # Call prune -B
       
    95     evolve = extensions.find('evolve')
       
    96     optsdict = {
       
    97         'new': [],
       
    98         'succ': [],
       
    99         'rev': [],
       
   100         'bookmark': bookmarks,
       
   101         'keep': None,
       
   102         'biject': False,
       
   103     }
       
   104     evolve.cmdprune(ui, repo, **optsdict)
       
   105 
       
   106 # obsolescence inhibitor
       
   107 ########################
       
   108 
       
   109 def _schedulewrite(tr, obsinhibit):
       
   110     """Make sure on disk content will be updated on transaction commit"""
       
   111     def writer(fp):
       
   112         """Serialize the inhibited list to disk.
       
   113         """
       
   114         raw = ''.join(obsinhibit)
       
   115         fp.write(raw)
       
   116     tr.addfilegenerator('obsinhibit', ('obsinhibit',), writer)
       
   117     tr.hookargs['obs_inbihited'] = '1'
       
   118 
       
   119 def _filterpublic(repo, nodes):
       
   120     """filter out inhibitor on public changeset
       
   121 
       
   122     Public changesets are already immune to obsolescence"""
       
   123     getrev = repo.changelog.nodemap.get
       
   124     getphase = repo._phasecache.phase
       
   125     return (n for n in nodes
       
   126             if getrev(n) is not None and getphase(repo, getrev(n)))
       
   127 
       
   128 def _inhibitmarkers(repo, nodes):
       
   129     """add marker inhibitor for all obsolete revision under <nodes>
       
   130 
       
   131     Content of <nodes> and all mutable ancestors are considered. Marker for
       
   132     obsolete revision only are created.
       
   133     """
       
   134     if not _inhibitenabled(repo):
       
   135         return
       
   136 
       
   137     # we add (non public()) as a lower boundary to
       
   138     # - use the C code in 3.6 (no ancestors in C as this is written)
       
   139     # - restrict the search space. Otherwise, the ancestors can spend a lot of
       
   140     #   time iterating if you have a check very low in the repo. We do not need
       
   141     #   to iterate over tens of thousand of public revisions with higher
       
   142     #   revision number
       
   143     #
       
   144     # In addition, the revset logic could be made significantly smarter here.
       
   145     newinhibit = repo.revs('(not public())::%ln and obsolete()', nodes)
       
   146     if newinhibit:
       
   147         node = repo.changelog.node
       
   148         lock = tr = None
       
   149         try:
       
   150             lock = repo.lock()
       
   151             tr = repo.transaction('obsinhibit')
       
   152             repo._obsinhibit.update(node(r) for r in newinhibit)
       
   153             _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
       
   154             repo.invalidatevolatilesets()
       
   155             tr.close()
       
   156         finally:
       
   157             lockmod.release(tr, lock)
       
   158 
       
   159 def _deinhibitmarkers(repo, nodes):
       
   160     """lift obsolescence inhibition on a set of nodes
       
   161 
       
   162     This will be triggered when inhibited nodes received new obsolescence
       
   163     markers. Otherwise the new obsolescence markers would also be inhibited.
       
   164     """
       
   165     if not _inhibitenabled(repo):
       
   166         return
       
   167 
       
   168     deinhibited = repo._obsinhibit & set(nodes)
       
   169     if deinhibited:
       
   170         tr = repo.transaction('obsinhibit')
       
   171         try:
       
   172             repo._obsinhibit -= deinhibited
       
   173             _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
       
   174             repo.invalidatevolatilesets()
       
   175             tr.close()
       
   176         finally:
       
   177             tr.release()
       
   178 
       
   179 def _createmarkers(orig, repo, relations, flag=0, date=None, metadata=None):
       
   180     """wrap markers create to make sure we de-inhibit target nodes"""
       
   181     # wrapping transactio to unify the one in each function
       
   182     lock = tr = None
       
   183     try:
       
   184         lock = repo.lock()
       
   185         tr = repo.transaction('add-obsolescence-marker')
       
   186         orig(repo, relations, flag, date, metadata)
       
   187         precs = (r[0].node() for r in relations)
       
   188         _deinhibitmarkers(repo, precs)
       
   189         tr.close()
       
   190     finally:
       
   191         lockmod.release(tr, lock)
       
   192 
       
   193 def _filterobsoleterevswrap(orig, repo, rebasesetrevs, *args, **kwargs):
       
   194     repo._notinhibited = rebasesetrevs
       
   195     try:
       
   196         repo.invalidatevolatilesets()
       
   197         r = orig(repo, rebasesetrevs, *args, **kwargs)
       
   198     finally:
       
   199         del repo._notinhibited
       
   200         repo.invalidatevolatilesets()
       
   201     return r
       
   202 
       
   203 def transactioncallback(orig, repo, desc, *args, **kwargs):
       
   204     """ Wrap localrepo.transaction to inhibit new obsolete changes """
       
   205     def inhibitposttransaction(transaction):
       
   206         # At the end of the transaction we catch all the new visible and
       
   207         # obsolete commit to inhibit them
       
   208         visibleobsolete = repo.revs('obsolete() - hidden()')
       
   209         ignoreset = set(getattr(repo, '_rebaseset', []))
       
   210         ignoreset |= set(getattr(repo, '_obsoletenotrebased', []))
       
   211         visibleobsolete = list(r for r in visibleobsolete if r not in ignoreset)
       
   212         if visibleobsolete:
       
   213             _inhibitmarkers(repo, [repo[r].node() for r in visibleobsolete])
       
   214     transaction = orig(repo, desc, *args, **kwargs)
       
   215     if desc != 'strip' and _inhibitenabled(repo):
       
   216         transaction.addpostclose('inhibitposttransaction',
       
   217                                  inhibitposttransaction)
       
   218     return transaction
       
   219 
       
   220 
       
   221 # We wrap these two functions to address the following scenario:
       
   222 # - Assuming that we have markers between commits in the rebase set and
       
   223 #   destination and that these markers are inhibited
       
   224 # - At the end of the rebase the nodes are still visible because rebase operate
       
   225 #   without inhibition and skip these nodes
       
   226 # We keep track in repo._obsoletenotrebased of the obsolete commits skipped by
       
   227 # the rebase and lift the inhibition in the end of the rebase.
       
   228 
       
   229 def _computeobsoletenotrebased(orig, repo, *args, **kwargs):
       
   230     r = orig(repo, *args, **kwargs)
       
   231     repo._obsoletenotrebased = r.keys()
       
   232     return r
       
   233 
       
   234 def _clearrebased(orig, ui, repo, *args, **kwargs):
       
   235     r = orig(ui, repo, *args, **kwargs)
       
   236     tonode = repo.changelog.node
       
   237     if util.safehasattr(repo, '_obsoletenotrebased'):
       
   238         _deinhibitmarkers(repo, [tonode(k) for k in repo._obsoletenotrebased])
       
   239     return r
       
   240 
       
   241 
       
   242 def extsetup(ui):
       
   243     # lets wrap the computation of the obsolete set
       
   244     # We apply inhibition there
       
   245     obsfunc = obsolete.cachefuncs['obsolete']
       
   246     def _computeobsoleteset(repo):
       
   247         """remove any inhibited nodes from the obsolete set
       
   248 
       
   249         This will trickle down to other part of mercurial (hidden, log, etc)"""
       
   250         obs = obsfunc(repo)
       
   251         if _inhibitenabled(repo):
       
   252             getrev = repo.changelog.nodemap.get
       
   253             blacklist = getattr(repo, '_notinhibited', set())
       
   254             for n in repo._obsinhibit:
       
   255                 if getrev(n) not in blacklist:
       
   256                     obs.discard(getrev(n))
       
   257         return obs
       
   258     try:
       
   259         extensions.find('directaccess')
       
   260     except KeyError:
       
   261         errormsg = _('cannot use inhibit without the direct access extension\n')
       
   262         hint = _("(please enable it or inhibit won\'t work)\n")
       
   263         ui.warn(errormsg)
       
   264         ui.warn(hint)
       
   265         return
       
   266 
       
   267     # Wrapping this to inhibit obsolete revs resulting from a transaction
       
   268     extensions.wrapfunction(localrepo.localrepository,
       
   269                             'transaction', transactioncallback)
       
   270 
       
   271     obsolete.cachefuncs['obsolete'] = _computeobsoleteset
       
   272     # wrap create marker to make it able to lift the inhibition
       
   273     extensions.wrapfunction(obsolete, 'createmarkers', _createmarkers)
       
   274     # drop divergence computation since it is incompatible with "light revive"
       
   275     obsolete.cachefuncs['divergent'] = lambda repo: set()
       
   276     # drop bumped computation since it is incompatible with "light revive"
       
   277     obsolete.cachefuncs['bumped'] = lambda repo: set()
       
   278     # wrap update to make sure that no obsolete commit is visible after an
       
   279     # update
       
   280     extensions.wrapcommand(commands.table, 'update', _update)
       
   281     try:
       
   282         rebase = extensions.find('rebase')
       
   283         if rebase:
       
   284             if util.safehasattr(rebase, '_filterobsoleterevs'):
       
   285                 extensions.wrapfunction(rebase,
       
   286                                         '_filterobsoleterevs',
       
   287                                         _filterobsoleterevswrap)
       
   288             extensions.wrapfunction(rebase, 'clearrebased', _clearrebased)
       
   289             if util.safehasattr(rebase, '_computeobsoletenotrebased'):
       
   290                 extensions.wrapfunction(rebase,
       
   291                                         '_computeobsoletenotrebased',
       
   292                                         _computeobsoletenotrebased)
       
   293 
       
   294     except KeyError:
       
   295         pass
       
   296     # There are two ways to save bookmark changes during a transation, we
       
   297     # wrap both to add inhibition markers.
       
   298     extensions.wrapfunction(bookmarks.bmstore, 'recordchange', _bookmarkchanged)
       
   299     if getattr(bookmarks.bmstore, 'write', None) is not None:# mercurial < 3.9
       
   300         extensions.wrapfunction(bookmarks.bmstore, 'write', _bookmarkchanged)
       
   301     # Add bookmark -D option
       
   302     entry = extensions.wrapcommand(commands.table, 'bookmark', _bookmark)
       
   303     entry[1].append(('D','prune',None,
       
   304                     _('delete the bookmark and prune the commits underneath')))
       
   305 
       
   306 @command('debugobsinhibit', [], '')
       
   307 def cmddebugobsinhibit(ui, repo, *revs):
       
   308     """inhibit obsolescence markers effect on a set of revs"""
       
   309     nodes = (repo[r].node() for r in scmutil.revrange(repo, revs))
       
   310     _inhibitmarkers(repo, nodes)