Documentation
¶
Overview ¶
Package gitmeta resolves per-file git metadata (last-commit time + author + subject, first-seen, churn, tracked/ignored status) for a single working tree by shelling out to the system `git` binary once per walk.
One Cache scans the entire repository up front (`git ls-files`, `git ls-files --others --ignored`, and a single `git log` pass keyed by HEAD), then answers per-path Lookup / IsTracked / IsIgnored in constant time. The batch architecture is dramatically cheaper than per-file `git log -1 -- <path>` on any non-trivial repo — a 10k-file tree with 5k commits costs one git invocation (~500ms), not 10k (~100s).
New returns a nil Cache (and nil error) when the supplied root isn't inside a git working tree, or when the `git` binary isn't on PATH — callers MUST handle nil and treat it as the "no git data; leave fields at their zero values" signal rather than a hard failure. A Pool (pool.go) caches one Cache per repo across calls and re-validates on HEAD change, so repeated lookups over an unchanging tree are free.
Requires the system `git` binary (HasGitBinary reports its presence).
Example ¶
Example shows the one-shot Cache: scan a working tree once, then answer per-file metadata lookups in constant time. (Output is omitted because commit times/authors are repo-specific.)
package main
import (
"context"
"fmt"
"github.com/richardwooding/gitmeta"
)
func main() {
ctx := context.Background()
cache, err := gitmeta.New(ctx, "/path/to/repo")
if err != nil {
panic(err)
}
if cache == nil {
fmt.Println("not a git working tree")
return
}
if info, ok := cache.Lookup("/path/to/repo/main.go"); ok {
fmt.Printf("last touched %s by %s — %d commits\n",
info.LastCommitTime.Format("2006-01-02"),
info.LastCommitAuthor,
info.CommitCount)
}
}
Output:
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func HasGitBinary ¶
func HasGitBinary() bool
HasGitBinary returns true when the `git` executable is on PATH. Useful for CLI callers that want to warn up front rather than silently produce empty git_* attributes. Cheap (one exec.LookPath).
Types ¶
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is the per-repository scan result. Build via New; consult via Lookup / IsTracked / IsIgnored. All read methods are safe for concurrent use (Cache is effectively immutable after construction).
func New ¶
New scans the git working tree containing root and returns a Cache. Returns nil, nil when root is not inside any git working tree (silent skip — the caller treats this as "no git data available"). Returns nil, err only on hard failures (git binary missing, subprocess crash, ctx cancellation).
The scan runs three git invocations concurrently after the initial rev-parse and waits for all of them. ctx propagates to each subprocess via exec.CommandContext, so a cancelled walk tears the git processes down promptly.
func (*Cache) HeadSHA ¶
HeadSHA returns the repository's HEAD commit SHA at the time the Cache was built. Useful for invalidation when the caller persists Cache results across processes.
func (*Cache) IsIgnored ¶
IsIgnored returns true when absPath is matched by .gitignore (or one of the other standard ignore-rule sources git consults) but not in the index. Tracked files are never reported as ignored even if a later .gitignore rule would have excluded them — that matches git's own `check-ignore` semantics.
func (*Cache) IsTracked ¶
IsTracked is the boolean-only form of Lookup: true when absPath is in git's index for this working tree.
func (*Cache) Lookup ¶
func (c *Cache) Lookup(absPath string) (FileGitInfo, bool)
Lookup returns git metadata for absPath. ok=false means absPath is not tracked by git in this working tree (either untracked, ignored, or outside the repo entirely). The caller should leave git_* CEL fields at their zero values in that case.
type FileGitInfo ¶
type FileGitInfo struct {
LastCommitTime time.Time
LastCommitAuthor string
LastCommitSubject string
FirstSeen time.Time
CommitCount int
}
FileGitInfo carries the per-file metadata Cache resolves for every tracked path in a working tree. Zero values are meaningful: a fresh commit's CommitCount==1 and FirstSeen==LastCommitTime.
type Pool ¶
type Pool struct {
// contains filtered or unexported fields
}
Pool caches *Cache instances by canonical repository root, refreshed when HEAD changes. Safe for concurrent use.
Designed to live for an MCP server's process lifetime so multiple `with_git=true` search calls against the same repo share one gitmeta.New() pass. HEAD invalidation runs on every Get — one `git rev-parse HEAD` per call (~3-5ms) — so a `git commit` or `git checkout` between calls is picked up without operator action.
Bounded memory in practice: MCP servers are typically cwd-anchored, so the map holds one entry per process. No LRU cap for v1.
Example ¶
ExamplePool shows reusing one Cache per repo across many calls; the pool re-validates on HEAD change, so an unchanged tree is scanned once.
package main
import (
"context"
"github.com/richardwooding/gitmeta"
)
func main() {
pool := gitmeta.NewPool()
ctx := context.Background()
for _, root := range []string{"/repo/a", "/repo/b", "/repo/a"} {
cache, err := pool.Get(ctx, root) // "/repo/a" the 2nd time is a cache hit
if err != nil || cache == nil {
continue
}
_ = cache.IsTracked(root + "/README.md")
}
}
Output:
func NewPool ¶
func NewPool() *Pool
NewPool returns an empty Pool ready for Get / Warm calls. Allocates only the underlying map — building actual caches happens on first Get. Cheap enough that the MCP server constructs one unconditionally.
func (*Pool) Get ¶
Get returns a Cache for the git tree containing root. If a cached entry exists for the same canonical repo root AND the current HEAD still matches, the cached Cache is returned unchanged. Otherwise (no entry, or HEAD moved since last Get) the cache is rebuilt via New and stored.
Returns (nil, nil) when root is not inside a git tree — same silent-skip contract as New. nil-receiver safe (returns nil, nil) for callers that don't always carry a pool.
Cost on a hit: one `git rev-parse HEAD` (~3-5ms) for the HEAD-sha staleness check. Cost on a miss: the full New() pass (~500ms on a 10k-file repo). The miss is paid only on the first call after process start or after a commit / checkout.
func (*Pool) Len ¶
Len returns the number of cached entries. Exposed for tests and future diagnostics (e.g. monitor_info "git pool: N repos").
func (*Pool) Warm ¶
Warm is Get-discard: pre-build the cache for root so the first search call doesn't pay the gitmeta.New() cost. Reuses Get's HEAD-validation logic, so a stale entry refreshes automatically. Returns the same nil-for-non-git-tree contract as Get; callers typically ignore the error and log the elapsed time.