# narrowrevlog.py - revlog storing irrelevant nodes as "ellipsis" nodes # # Copyright 2017 Google, Inc. # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from __future__ import absolute_import from mercurial import ( error, manifest, revlog, util, ) def readtransform(self, text): return text, False def writetransform(self, text): return text, False def rawtransform(self, text): return False revlog.addflagprocessor(revlog.REVIDX_ELLIPSIS, (readtransform, writetransform, rawtransform)) def setup(): # We just wanted to add the flag processor, which is done at module # load time. pass class excludeddir(manifest.treemanifest): """Stand-in for a directory that is excluded from the repository. With narrowing active on a repository that uses treemanifests, some of the directory revlogs will be excluded from the resulting clone. This is a huge storage win for clients, but means we need some sort of pseudo-manifest to surface to internals so we can detect a merge conflict outside the narrowspec. That's what this class is: it stands in for a directory whose node is known, but whose contents are unknown. """ def __init__(self, dir, node): super(excludeddir, self).__init__(dir) self._node = node # Add an empty file, which will be included by iterators and such, # appearing as the directory itself (i.e. something like "dir/") self._files[''] = node self._flags[''] = 't' # Manifests outside the narrowspec should never be modified, so avoid # copying. This makes a noticeable difference when there are very many # directories outside the narrowspec. Also, it makes sense for the copy to # be of the same type as the original, which would not happen with the # super type's copy(). def copy(self): return self class excludeddirmanifestctx(manifest.treemanifestctx): """context wrapper for excludeddir - see that docstring for rationale""" def __init__(self, dir, node): self._dir = dir self._node = node def read(self): return excludeddir(self._dir, self._node) def write(self, *args): raise error.ProgrammingError( 'attempt to write manifest from excluded dir %s' % self._dir) class excludedmanifestrevlog(manifest.manifestrevlog): """Stand-in for excluded treemanifest revlogs. When narrowing is active on a treemanifest repository, we'll have references to directories we can't see due to the revlog being skipped. This class exists to conform to the manifestrevlog interface for those directories and proactively prevent writes to outside the narrowspec. """ def __init__(self, dir): self._dir = dir def __len__(self): raise error.ProgrammingError( 'attempt to get length of excluded dir %s' % self._dir) def rev(self, node): raise error.ProgrammingError( 'attempt to get rev from excluded dir %s' % self._dir) def linkrev(self, node): raise error.ProgrammingError( 'attempt to get linkrev from excluded dir %s' % self._dir) def node(self, rev): raise error.ProgrammingError( 'attempt to get node from excluded dir %s' % self._dir) def add(self, *args, **kwargs): # We should never write entries in dirlogs outside the narrow clone. # However, the method still gets called from writesubtree() in # _addtree(), so we need to handle it. We should possibly make that # avoid calling add() with a clean manifest (_dirty is always False # in excludeddir instances). pass def makenarrowmanifestrevlog(mfrevlog, repo): if util.safehasattr(mfrevlog, '_narrowed'): return class narrowmanifestrevlog(mfrevlog.__class__): # This function is called via debug{revlog,index,data}, but also during # at least some push operations. This will be used to wrap/exclude the # child directories when using treemanifests. def dirlog(self, d): if d and not d.endswith('/'): d = d + '/' if not repo.narrowmatch().visitdir(d[:-1] or '.'): return excludedmanifestrevlog(d) result = super(narrowmanifestrevlog, self).dirlog(d) makenarrowmanifestrevlog(result, repo) return result mfrevlog.__class__ = narrowmanifestrevlog mfrevlog._narrowed = True def makenarrowmanifestlog(mfl, repo): class narrowmanifestlog(mfl.__class__): def get(self, dir, node, verify=True): if not repo.narrowmatch().visitdir(dir[:-1] or '.'): return excludeddirmanifestctx(dir, node) return super(narrowmanifestlog, self).get(dir, node, verify=verify) mfl.__class__ = narrowmanifestlog def makenarrowfilelog(fl, narrowmatch): class narrowfilelog(fl.__class__): def renamed(self, node): # Renames that come from outside the narrowspec are # problematic at least for git-diffs, because we lack the # base text for the rename. This logic was introduced in # 3cd72b1 of narrowhg (authored by martinvonz, reviewed by # adgar), but that revision doesn't have any additional # commentary on what problems we can encounter. m = super(narrowfilelog, self).renamed(node) if m and not narrowmatch(m[0]): return None return m def size(self, rev): # We take advantage of the fact that remotefilelog # lacks a node() method to just skip the # rename-checking logic when on remotefilelog. This # might be incorrect on other non-revlog-based storage # engines, but for now this seems to be fine. # # TODO: when remotefilelog is in core, improve this to # explicitly look for remotefilelog instead of cheating # with a hasattr check. if util.safehasattr(self, 'node'): node = self.node(rev) # Because renamed() is overridden above to # sometimes return None even if there is metadata # in the revlog, size can be incorrect for # copies/renames, so we need to make sure we call # the super class's implementation of renamed() # for the purpose of size calculation. if super(narrowfilelog, self).renamed(node): return len(self.read(node)) return super(narrowfilelog, self).size(rev) def cmp(self, node, text): different = super(narrowfilelog, self).cmp(node, text) if different: # Similar to size() above, if the file was copied from # a file outside the narrowspec, the super class's # would have returned True because we tricked it into # thinking that the file was not renamed. if super(narrowfilelog, self).renamed(node): t2 = self.read(node) return t2 != text return different fl.__class__ = narrowfilelog