Mercurial > evolve
view hgext/inhibit.py @ 1338:77cbf9121e8a
inhibit: handle inhibit marker on stripped revision
If a revision disappear from the repo, we should not crash.
author | Pierre-Yves David <pierre-yves.david@fb.com> |
---|---|
date | Thu, 14 May 2015 15:59:06 -0700 |
parents | b8f880d4171d |
children | 0e2eb196923a |
line wrap: on
line source
"""Reduce the changesets evolution feature scope for early and noob friendly UI The full scale changeset evolution have some massive bleeding edge and it is very easy for people not very intimate with the concept to end up in intricate situation. In order to get some of the benefit sooner, this extension is disabling some of the less polished aspect of evolution. It should gradually get thinner and thinner as changeset evolution will get more polished. This extension is only recommended for large scale organisations. Individual user should probably stick on using Evolution in its current state, understand its concept and provide feedback The first feature provided by this extension is the ability to "inhibit" obsolescence markers. Obsolete revision can be cheaply brought back to life that way. However as the inhibitor are not fitting in an append only model, this is incompatible with sharing mutable history. The second feature is called direct access. It is the ability to refer and access hidden sha in commands provided that you know their value. For example hg log -r XXX where XXX is a commit has should work whether XXX is hidden or not as we assume that the user knows what he is doing when referring to XXX. """ from mercurial import localrepo from mercurial import obsolete from mercurial import extensions from mercurial import cmdutil from mercurial import scmutil from mercurial import repoview from mercurial import branchmap from mercurial import revset from mercurial import error from mercurial import commands from mercurial import lock as lockmod from mercurial import bookmarks from mercurial import lock as lockmod from mercurial.i18n import _ cmdtable = {} command = cmdutil.command(cmdtable) # List of commands where no warning is shown for direct access directaccesslevel = [ # warning or not, extension (None if core), command name (False, None, 'update'), (False, None, 'export'), (True, 'rebase', 'rebase'), (False, 'evolve', 'prune'), ] def reposetup(ui, repo): class obsinhibitedrepo(repo.__class__): @localrepo.storecache('obsinhibit') def _obsinhibit(self): # XXX we should make sure it is invalidated by transaction failure obsinhibit = set() raw = self.sopener.tryread('obsinhibit') for i in xrange(0, len(raw), 20): obsinhibit.add(raw[i:i+20]) return obsinhibit def commit(self, *args, **kwargs): newnode = super(obsinhibitedrepo, self).commit(*args, **kwargs) _inhibitmarkers(repo, [newnode]) return newnode repo.__class__ = obsinhibitedrepo repo._explicitaccess = set() if not ui.configbool('inhibit', 'onlydirectaccess', False): # Wrapping this to inhibit obsolete revs resulting from a transaction extensions.wrapfunction(localrepo.localrepository, 'transaction', transactioncallback) def _computehidden(repo): hidden = repoview.computehidden(repo) cl = repo.changelog dynamic = hidden & repo._explicitaccess if dynamic: blocked = cl.ancestors(dynamic, inclusive=True) hidden = frozenset(r for r in hidden if r not in blocked) return hidden def setupdirectaccess(): """ Add two new filtername that behave like visible to provide direct access and direct access with warning. Wraps the commands to setup direct access """ repoview.filtertable.update({'visible-directaccess-nowarn': _computehidden}) repoview.filtertable.update({'visible-directaccess-warn': _computehidden}) branchmap.subsettable['visible-directaccess-nowarn'] = 'visible' branchmap.subsettable['visible-directaccess-warn'] = 'visible' for warn, ext, cmd in directaccesslevel: cmdtable = extensions.find(ext).cmdtable if ext else commands.table wrapper = wrapwithwarning if warn else wrapwithoutwarning try: extensions.wrapcommand(cmdtable, cmd, wrapper) except error.UnknownCommand: pass def _update(orig, ui, repo, *args, **kwargs): """ When moving to a commit we want to inhibit any obsolete commit affecting the changeset we are updating to. In other words we don't want any visible commit to be obsolete. """ wlock = None try: # Evolve is running a hook on lock release to display a warning message # if the workind dir's parent is obsolete. # We take the lock here to make sure that we inhibit the parent before # that hook get a chance to run. wlock = repo.wlock() res = orig(ui, repo, *args, **kwargs) newhead = repo['.'].node() _inhibitmarkers(repo, [newhead]) return res finally: lockmod.release(wlock) def _bookmarkchanged(orig, bkmstoreinst, *args, **kwargs): """ Add inhibition markers to every obsolete bookmarks """ repo = bkmstoreinst._repo bkmstorenodes = [repo[v].node() for v in bkmstoreinst.values()] _inhibitmarkers(repo, bkmstorenodes) return orig(bkmstoreinst, *args, **kwargs) def _bookmark(orig, ui, repo, *bookmarks, **opts): """ Add a -D option to the bookmark command, map it to prune -B """ haspruneopt = opts.get('prune', False) if not haspruneopt: return orig(ui, repo, *bookmarks, **opts) # Call prune -B evolve = extensions.find('evolve') optsdict = { 'new': [], 'succ': [], 'rev': [], 'bookmark': bookmarks[0], 'keep': None, 'biject': False, } evolve.cmdprune(ui, repo, **optsdict) # obsolescence inhibitor ######################## def _schedulewrite(tr, obsinhibit): """Make sure on disk content will be updated on transaction commit""" def writer(fp): """Serialize the inhibited list to disk. """ raw = ''.join(obsinhibit) fp.write(raw) tr.addfilegenerator('obsinhibit', ('obsinhibit',), writer) tr.hookargs['obs_inbihited'] = '1' def _filterpublic(repo, nodes): """filter out inhibitor on public changeset Public changesets are already immune to obsolescence""" getrev = repo.changelog.nodemap.get getphase = repo._phasecache.phase return (n for n in repo._obsinhibit if getrev(n) is not None and getphase(repo, getrev(n))) def _inhibitmarkers(repo, nodes): """add marker inhibitor for all obsolete revision under <nodes> Content of <nodes> and all mutable ancestors are considered. Marker for obsolete revision only are created. """ newinhibit = repo.set('::%ln and obsolete()', nodes) if newinhibit: lock = tr = None try: lock = repo.lock() tr = repo.transaction('obsinhibit') repo._obsinhibit.update(c.node() for c in newinhibit) _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit)) repo.invalidatevolatilesets() tr.close() finally: lockmod.release(tr, lock) def _deinhibitmarkers(repo, nodes): """lift obsolescence inhibition on a set of nodes This will be triggered when inhibited nodes received new obsolescence markers. Otherwise the new obsolescence markers would also be inhibited. """ deinhibited = repo._obsinhibit & set(nodes) if deinhibited: tr = repo.transaction('obsinhibit') try: repo._obsinhibit -= deinhibited _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit)) repo.invalidatevolatilesets() tr.close() finally: tr.release() def _createmarkers(orig, repo, relations, flag=0, date=None, metadata=None): """wrap markers create to make sure we de-inhibit target nodes""" # wrapping transactio to unify the one in each function tr = repo.transaction('add-obsolescence-marker') try: orig(repo, relations, flag, date, metadata) precs = (r[0].node() for r in relations) _deinhibitmarkers(repo, precs) tr.close() finally: tr.release() def transactioncallback(orig, repo, *args, **kwargs): """ Wrap localrepo.transaction to inhibit new obsolete changes """ def inhibitposttransaction(transaction): # At the end of the transaction we catch all the new visible and # obsolete commit to inhibit them visibleobsolete = repo.revs('(not hidden()) and obsolete()') ignoreset = set(getattr(repo, '_rebaseset', [])) visibleobsolete = list(r for r in visibleobsolete if r not in ignoreset) if visibleobsolete: _inhibitmarkers(repo, [repo[r].node() for r in visibleobsolete]) transaction = orig(repo, *args, **kwargs) transaction.addpostclose('inhibitposttransaction', inhibitposttransaction) return transaction def wrapwithoutwarning(orig, ui, repo, *args, **kwargs): if repo and repo.filtername == 'visible': repo = repo.filtered("visible-directaccess-nowarn") return orig(ui, repo, *args, **kwargs) def wrapwithwarning(orig, ui, repo, *args, **kwargs): if repo and repo.filtername == 'visible': repo = repo.filtered("visible-directaccess-warn") return orig(ui, repo, *args, **kwargs) def extsetup(ui): # lets wrap the computation of the obsolete set # We apply inhibition there obsfunc = obsolete.cachefuncs['obsolete'] def _computeobsoleteset(repo): """remove any inhibited nodes from the obsolete set This will trickle down to other part of mercurial (hidden, log, etc)""" obs = obsfunc(repo) getrev = repo.changelog.nodemap.get for n in repo._obsinhibit: obs.discard(getrev(n)) return obs obsolete.cachefuncs['obsolete'] = _computeobsoleteset # wrap create marker to make it able to lift the inhibition extensions.wrapfunction(obsolete, 'createmarkers', _createmarkers) extensions.wrapfunction(revset, 'posttreebuilthook', _posttreebuilthook) setupdirectaccess() if not ui.configbool('inhibit', 'onlydirectaccess', False): # drop divergence computation since it is incompatible with "light revive" obsolete.cachefuncs['divergent'] = lambda repo: set() # drop bumped computation since it is incompatible with "light revive" obsolete.cachefuncs['bumped'] = lambda repo: set() # wrap update to make sure that no obsolete commit is visible after an # update extensions.wrapcommand(commands.table, 'update', _update) # There are two ways to save bookmark changes during a transation, we # wrap both to add inhibition markers. extensions.wrapfunction(bookmarks.bmstore, 'recordchange', _bookmarkchanged) extensions.wrapfunction(bookmarks.bmstore, 'write', _bookmarkchanged) # Add bookmark -D option entry = extensions.wrapcommand(commands.table, 'bookmark', _bookmark) entry[1].append(('D','prune',None, _('delete the bookmark and prune the commits underneath'))) def gethashsymbols(tree): # Returns the list of symbols of the tree that look like hashes # for example for the revset 3::abe3ff it will return ('abe3ff') if not tree: return [] if len(tree) == 2 and tree[0] == "symbol": try: int(tree[1]) return [] except ValueError as e: return [tree[1]] elif len(tree) == 3: return gethashsymbols(tree[1]) + gethashsymbols(tree[2]) else: return [] def _posttreebuilthook(orig, tree, repo): # This is use to enabled direct hash access # We extract the symbols that look like hashes and add them to the # explicitaccess set orig(tree, repo) filternm = "" if repo is not None: filternm = repo.filtername if filternm is not None and filternm.startswith('visible-directaccess'): prelength = len(repo._explicitaccess) accessbefore = set(repo._explicitaccess) repo.symbols = gethashsymbols(tree) cl = repo.unfiltered().changelog for node in repo.symbols: try: node = cl._partialmatch(node) except error.LookupError: node = None if node is not None: rev = cl.rev(node) if rev not in repo.changelog: repo._explicitaccess.add(rev) if prelength != len(repo._explicitaccess): if repo.filtername != 'visible-directaccess-nowarn': unhiddencommits = repo._explicitaccess - accessbefore repo.ui.warn( _("Warning: accessing hidden changesets %s " "for write operation\n") % (",".join([str(repo.unfiltered()[l]) for l in unhiddencommits]))) repo.invalidatevolatilesets() @command('debugobsinhibit', [], '') def cmddebugobsinhibit(ui, repo, *revs): """inhibit obsolescence markers effect on a set of revs""" nodes = (repo[r].node() for r in scmutil.revrange(repo, revs)) _inhibitmarkers(repo, nodes)