context: add overlayworkingcontext and overlayworkingfilectx
authorPhil Cohen <phillco@fb.com>
Mon, 11 Sep 2017 13:03:27 -0700
changeset 34104 f698bb31bdfb
parent 34103 92f1e2be8ab6
child 34105 c037fd655b47
context: add overlayworkingcontext and overlayworkingfilectx These two classes will be used extensively in the first in-memory merge milestone. Differential Revision: https://phab.mercurial-scm.org/D616
mercurial/context.py
--- a/mercurial/context.py	Sun Sep 10 18:52:40 2017 -0700
+++ b/mercurial/context.py	Mon Sep 11 13:03:27 2017 -0700
@@ -1661,6 +1661,9 @@
                               listsubrepos=listsubrepos, badfn=badfn,
                               icasefs=icasefs)
 
+    def flushall(self):
+        pass # For overlayworkingfilectx compatibility.
+
     def _filtersuspectsymlink(self, files):
         if not files or self._repo.dirstate._checklink:
             return files
@@ -1974,6 +1977,191 @@
     def setflags(self, l, x):
         self._repo.wvfs.setflags(self._path, l, x)
 
+class overlayworkingctx(workingctx):
+    """Wraps another mutable context with a write-back cache that can be flushed
+    at a later time.
+
+    self._cache[path] maps to a dict with keys: {
+        'exists': bool?
+        'date': date?
+        'data': str?
+        'flags': str?
+    }
+    If `exists` is True, `flags` must be non-None and 'date' is non-None. If it
+    is `False`, the file was deleted.
+    """
+
+    def __init__(self, repo, wrappedctx):
+        super(overlayworkingctx, self).__init__(repo)
+        self._repo = repo
+        self._wrappedctx = wrappedctx
+        self._clean()
+
+    def data(self, path):
+        if self.isdirty(path):
+            if self._cache[path]['exists']:
+                if self._cache[path]['data']:
+                    return self._cache[path]['data']
+                else:
+                    # Must fallback here, too, because we only set flags.
+                    return self._wrappedctx[path].data()
+            else:
+                raise error.ProgrammingError("No such file or directory: %s" %
+                                             self._path)
+        else:
+            return self._wrappedctx[path].data()
+
+    def filedate(self, path):
+        if self.isdirty(path):
+            return self._cache[path]['date']
+        else:
+            return self._wrappedctx[path].date()
+
+    def flags(self, path):
+        if self.isdirty(path):
+            if self._cache[path]['exists']:
+                return self._cache[path]['flags']
+            else:
+                raise error.ProgrammingError("No such file or directory: %s" %
+                                             self._path)
+        else:
+            return self._wrappedctx[path].flags()
+
+    def write(self, path, data, flags=''):
+        if data is None:
+            raise error.ProgrammingError("data must be non-None")
+        self._markdirty(path, exists=True, data=data, date=util.makedate(),
+                        flags=flags)
+
+    def setflags(self, path, l, x):
+        self._markdirty(path, exists=True, date=util.makedate(),
+                        flags=(l and 'l' or '') + (x and 'x' or ''))
+
+    def remove(self, path):
+        self._markdirty(path, exists=False)
+
+    def exists(self, path):
+        """exists behaves like `lexists`, but needs to follow symlinks and
+        return False if they are broken.
+        """
+        if self.isdirty(path):
+            # If this path exists and is a symlink, "follow" it by calling
+            # exists on the destination path.
+            if (self._cache[path]['exists'] and
+                        'l' in self._cache[path]['flags']):
+                return self.exists(self._cache[path]['data'].strip())
+            else:
+                return self._cache[path]['exists']
+        return self._wrappedctx[path].exists()
+
+    def lexists(self, path):
+        """lexists returns True if the path exists"""
+        if self.isdirty(path):
+            return self._cache[path]['exists']
+        return self._wrappedctx[path].lexists()
+
+    def size(self, path):
+        if self.isdirty(path):
+            if self._cache[path]['exists']:
+                return len(self._cache[path]['data'])
+            else:
+                raise error.ProgrammingError("No such file or directory: %s" %
+                                             self._path)
+        return self._wrappedctx[path].size()
+
+    def flushall(self):
+        for path in self._writeorder:
+            entry = self._cache[path]
+            if entry['exists']:
+                self._wrappedctx[path].clearunknown()
+                if entry['data'] is not None:
+                    if entry['flags'] is None:
+                        raise error.ProgrammingError('data set but not flags')
+                    self._wrappedctx[path].write(
+                        entry['data'],
+                        entry['flags'])
+                else:
+                    self._wrappedctx[path].setflags(
+                        'l' in entry['flags'],
+                        'x' in entry['flags'])
+            else:
+                self._wrappedctx[path].remove(path)
+        self._clean()
+
+    def isdirty(self, path):
+        return path in self._cache
+
+    def _clean(self):
+        self._cache = {}
+        self._writeorder = []
+
+    def _markdirty(self, path, exists, data=None, date=None, flags=''):
+        if path not in self._cache:
+            self._writeorder.append(path)
+
+        self._cache[path] = {
+            'exists': exists,
+            'data': data,
+            'date': date,
+            'flags': flags,
+        }
+
+    def filectx(self, path, filelog=None):
+        return overlayworkingfilectx(self._repo, path, parent=self,
+                                     filelog=filelog)
+
+class overlayworkingfilectx(workingfilectx):
+    """Wrap a ``workingfilectx`` but intercepts all writes into an in-memory
+    cache, which can be flushed through later by calling ``flush()``."""
+
+    def __init__(self, repo, path, filelog=None, parent=None):
+        super(overlayworkingfilectx, self).__init__(repo, path, filelog,
+                                                    parent)
+        self._repo = repo
+        self._parent = parent
+        self._path = path
+
+    def ctx(self):
+        return self._parent
+
+    def data(self):
+        return self._parent.data(self._path)
+
+    def date(self):
+        return self._parent.filedate(self._path)
+
+    def exists(self):
+        return self.lexists()
+
+    def lexists(self):
+        return self._parent.exists(self._path)
+
+    def renamed(self):
+        # Copies are currently tracked in the dirstate as before. Straight copy
+        # from workingfilectx.
+        rp = self._repo.dirstate.copied(self._path)
+        if not rp:
+            return None
+        return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
+
+    def size(self):
+        return self._parent.size(self._path)
+
+    def audit(self):
+        pass
+
+    def flags(self):
+        return self._parent.flags(self._path)
+
+    def setflags(self, islink, isexec):
+        return self._parent.setflags(self._path, islink, isexec)
+
+    def write(self, data, flags, backgroundclose=False):
+        return self._parent.write(self._path, data, flags)
+
+    def remove(self, ignoremissing=False):
+        return self._parent.remove(self._path)
+
 class workingcommitctx(workingctx):
     """A workingcommitctx object makes access to data related to
     the revision being committed convenient.