# HG changeset patch # User Phil Cohen # Date 2017-09-11 20:03:27 # Node ID f698bb31bdfb74278ec76c31910e6548ac7a9834 # Parent 92f1e2be8ab65bec1e4dd17108b7bbbfe8607a80 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 diff --git a/mercurial/context.py b/mercurial/context.py --- a/mercurial/context.py +++ b/mercurial/context.py @@ -1661,6 +1661,9 @@ class workingctx(committablectx): 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 @@ class workingfilectx(committablefilectx) 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.