feat(gitshallow): add MaxAge gate via FETCH_HEAD mtime

Short-lived CLI invocations were doing a full git fetch+reset on every
run because the only debounce was an in-memory lastSynced field. MaxAge
skips the fetch when .git/FETCH_HEAD is younger than the configured
duration — git rewrites FETCH_HEAD on every successful fetch, so its
mtime is effectively "last time we talked to the remote", and it
survives process restart. Wire check-ip's blocklist repo to the same
47m refresh interval it uses for the background Tick.
This commit is contained in:
AJ ONeal 2026-04-20 17:28:53 -06:00
parent 631f32cf95
commit 5e6688c2a9
No known key found for this signature in database
2 changed files with 21 additions and 3 deletions

View File

@ -120,6 +120,7 @@ func main() {
// Blocklists: git repo with inbound + outbound IP cohort files.
repo := gitshallow.New(cfg.RepoURL, filepath.Join(cfg.CacheDir, "bitwire-it"), 1, "")
repo.MaxAge = refreshInterval
blocklists := dataset.NewSet(repo)
cfg.inbound = dataset.Add(blocklists, func() (*ipcohort.Cohort, error) {
return ipcohort.LoadFiles(

View File

@ -17,15 +17,21 @@ type Repo struct {
Depth int // 0 defaults to 1, -1 for all
Branch string // Optional: specific branch to clone/pull
// MaxAge skips the git fetch when .git/FETCH_HEAD is younger than this
// duration. Persists across process restarts (unlike the in-memory
// lastSynced debounce) — so repeated short-lived CLI invocations don't
// hammer the remote. 0 disables.
MaxAge time.Duration
// GCInterval controls explicit aggressive GC after pulls.
// 0 (default) — no explicit gc; git runs gc.auto on its own schedule
// 1 — aggressive gc after every pull
// N — aggressive gc after every Nth pull
GCInterval int
mu sync.Mutex
pullCount int
lastSynced time.Time
mu sync.Mutex
pullCount int
lastSynced time.Time
}
// New creates a new Repo instance.
@ -238,6 +244,17 @@ func (f *File) Fetch() (bool, error) {
}
func (r *Repo) syncGit() (updated bool, err error) {
// MaxAge: file-mtime gate (FETCH_HEAD is rewritten on every successful
// fetch, so its mtime is "last time we talked to the remote"). Checked
// outside the lock — just a stat.
if r.MaxAge > 0 {
if info, err := os.Stat(filepath.Join(r.Path, ".git", "FETCH_HEAD")); err == nil {
if time.Since(info.ModTime()) < r.MaxAge {
return false, nil
}
}
}
r.mu.Lock()
defer r.mu.Unlock()