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.
75 lines
2.3 KiB
Python
75 lines
2.3 KiB
Python
"""The snapshot of a repository that every plugin reads."""
|
|
|
|
from collections import namedtuple
|
|
|
|
LastCommit = namedtuple("LastCommit", "hash author relativeDate subject")
|
|
|
|
|
|
class RepoInfo:
|
|
"""Everything one `git status` and one `git log -1` can tell us.
|
|
|
|
Instances are immutable in practice: the cache hands the same object to
|
|
every caller for the lifetime of a cache entry, so plugins must treat
|
|
the fields as read-only.
|
|
|
|
When `error` is set, every other field is at its zero value and must not
|
|
be displayed as fact.
|
|
"""
|
|
|
|
__slots__ = ("root", "branch", "upstream", "ahead", "behind", "hasUpstream",
|
|
"staged", "unstaged", "untracked", "lastCommit", "error")
|
|
|
|
def __init__(self, root=None):
|
|
self.root = root
|
|
self.branch = None
|
|
self.upstream = None
|
|
self.ahead = 0
|
|
self.behind = 0
|
|
self.hasUpstream = False
|
|
self.staged = 0
|
|
self.unstaged = 0
|
|
self.untracked = 0
|
|
self.lastCommit = None
|
|
self.error = None
|
|
|
|
@property
|
|
def dirty(self):
|
|
return (self.staged + self.unstaged + self.untracked) > 0
|
|
|
|
@property
|
|
def unpushed(self):
|
|
"""Commits the remote does not have, including "there is no remote"."""
|
|
return self.ahead > 0 or not self.hasUpstream
|
|
|
|
@property
|
|
def detached(self):
|
|
return self.branch in (None, "(detached)")
|
|
|
|
@property
|
|
def branchLabel(self):
|
|
return self.branch or "(detached)"
|
|
|
|
@property
|
|
def summary(self):
|
|
"""One ASCII line combining sync state and working tree state."""
|
|
if self.error:
|
|
return self.error
|
|
parts = []
|
|
if self.ahead:
|
|
parts.append("%d ahead" % self.ahead)
|
|
if self.behind:
|
|
parts.append("%d behind" % self.behind)
|
|
if not self.hasUpstream:
|
|
parts.append("no upstream")
|
|
treeParts = []
|
|
if self.staged or self.unstaged:
|
|
treeParts.append("%d changed" % (self.staged + self.unstaged))
|
|
if self.untracked:
|
|
treeParts.append("%d untracked" % self.untracked)
|
|
if treeParts:
|
|
parts.append(", ".join(treeParts))
|
|
return ", ".join(parts) if parts else "clean"
|
|
|
|
def __repr__(self):
|
|
return "<RepoInfo %s %s: %s>" % (self.root, self.branchLabel, self.summary)
|