The single file extension carried 260 lines of infrastructure under its
four Nemo providers: the config file with live reload, the git query
layer, the TTL cache with .git watching, and repository detection. Any
second extension wanting repository awareness had to repeat all of it.
Move that foundation into a nemoRepoCore package with a deliberate plugin
API rather than whatever the providers happened to leave behind:
* Config gains layered defaults, so a plugin registers its own keys and
an empty config file is still a working one
* RepoInfo gains a root, a named last commit tuple and a summary string
* RepoCache gains refresh, invalidate and an invalidation callback
* discovery separates the cheap root check used on every drawn folder
from the upward walk used by on demand work such as menus
* nemoFiles collapses the uri scheme and directory guards every
provider was repeating
* launchers learn the per emulator flag for running a command
* version negotiation lets a plugin state the core it was built for
The config file moves to nemo-repo-core.json and is seeded from the old
nemo-repo-emblems.json when one exists, so no key changes name and
nothing needs editing by hand.
111 lines
3.8 KiB
Python
111 lines
3.8 KiB
Python
"""TTL cache in front of git, invalidated eagerly by watching .git.
|
|
|
|
Without this, Nemo would fork a git process for every folder it draws, on
|
|
every redraw. With it, a repository is queried at most once per TTL, and the
|
|
entry is dropped the moment .git changes so a commit or checkout shows up
|
|
immediately instead of after the TTL.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import weakref
|
|
|
|
from gi.repository import Gio, GLib
|
|
|
|
from .Config import coreConfig
|
|
from .gitQuery import computeRepoInfo
|
|
from .logs import pluginLogger
|
|
|
|
logger = pluginLogger("cache")
|
|
|
|
|
|
class RepoCache:
|
|
"""Keyed by repository root path. Shared by every plugin in the family."""
|
|
|
|
def __init__(self, ttlProvider):
|
|
self._ttlProvider = ttlProvider
|
|
self._entries = {}
|
|
self._monitors = {}
|
|
self._trackedFiles = {}
|
|
self._invalidateCallbacks = []
|
|
|
|
def get(self, repoRoot):
|
|
"""Cached RepoInfo for `repoRoot`, computing it when stale or absent."""
|
|
now = time.monotonic()
|
|
entry = self._entries.get(repoRoot)
|
|
if entry is not None:
|
|
info, storedAt = entry
|
|
if (now - storedAt) < self._ttlProvider():
|
|
return info
|
|
return self.refresh(repoRoot)
|
|
|
|
def refresh(self, repoRoot):
|
|
"""Recompute now, ignoring the TTL, and store the result."""
|
|
info = computeRepoInfo(repoRoot)
|
|
self._entries[repoRoot] = (info, time.monotonic())
|
|
self._ensureWatch(repoRoot)
|
|
return info
|
|
|
|
def invalidate(self, repoRoot=None):
|
|
"""Drop one entry, or the whole cache when called without argument."""
|
|
if repoRoot is None:
|
|
roots = list(self._entries)
|
|
self._entries.clear()
|
|
else:
|
|
roots = [repoRoot] if self._entries.pop(repoRoot, None) else []
|
|
for root in roots:
|
|
self._notify(root)
|
|
|
|
def registerFileObject(self, repoRoot, nemoFile):
|
|
"""Track a live Nemo.FileInfo so it can be redrawn on git changes.
|
|
|
|
References are weak: Nemo owns the object's lifetime, we only ask it
|
|
to re-query us when the repository underneath it moves.
|
|
"""
|
|
tracked = self._trackedFiles.setdefault(repoRoot, weakref.WeakSet())
|
|
try:
|
|
tracked.add(nemoFile)
|
|
except TypeError:
|
|
logger.debug("Object for %s is not weak-referenceable", repoRoot)
|
|
|
|
def onInvalidate(self, callback):
|
|
"""Register `callback(repoRoot)`, run whenever an entry is dropped."""
|
|
self._invalidateCallbacks.append(callback)
|
|
|
|
# -- internals ----------------------------------------------------------
|
|
|
|
def _notify(self, repoRoot):
|
|
for nemoFile in list(self._trackedFiles.get(repoRoot, ())):
|
|
nemoFile.invalidate_extension_info()
|
|
for callback in list(self._invalidateCallbacks):
|
|
callback(repoRoot)
|
|
|
|
def _ensureWatch(self, repoRoot):
|
|
if repoRoot in self._monitors:
|
|
return
|
|
gitDirectory = os.path.join(repoRoot, ".git")
|
|
if not os.path.isdir(gitDirectory):
|
|
return
|
|
|
|
monitors = []
|
|
for path in (gitDirectory, os.path.join(gitDirectory, "refs", "heads")):
|
|
if not os.path.exists(path):
|
|
continue
|
|
try:
|
|
gfile = Gio.File.new_for_path(path)
|
|
monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None)
|
|
except GLib.Error as error:
|
|
logger.debug("Cannot watch %s: %s", path, error)
|
|
continue
|
|
monitor.connect("changed", self._onGitDirectoryChanged, repoRoot)
|
|
monitors.append(monitor)
|
|
if monitors:
|
|
self._monitors[repoRoot] = monitors
|
|
|
|
def _onGitDirectoryChanged(self, monitor, gfile, otherFile, eventType, repoRoot):
|
|
self._entries.pop(repoRoot, None)
|
|
self._notify(repoRoot)
|
|
|
|
|
|
repoCache = RepoCache(lambda: coreConfig.ttlSeconds)
|