feat: expose refresh outcomes

This commit is contained in:
AJ ONeal 2026-09-19 01:50:36 -06:00
parent c6da1e7e4a
commit a43a722e79
No known key found for this signature in database
8 changed files with 44 additions and 28 deletions

View File

@ -108,7 +108,10 @@ func main() {
os.Exit(1)
}
state := "already up to date"
if updated {
switch updated {
case gitshallow.UpdateSkipped:
state = "skipped"
case gitshallow.UpdateChanged:
state = "updated"
}
fmt.Fprintf(os.Stderr, "%s (%s)\n", time.Since(t).Round(time.Millisecond), state)

View File

@ -59,8 +59,8 @@ func TestRepo_ForcePushRecovery(t *testing.T) {
repo := gitshallow.New(upstream, clone, 1, "main")
if updated, err := repo.Update(t.Context()); err != nil {
t.Fatalf("first Update: %v", err)
} else if !updated {
t.Fatal("first Update: expected updated=true")
} else if updated != gitshallow.UpdateChanged {
t.Fatal("first Update: expected changed")
}
got, err := os.ReadFile(filepath.Join(clone, "data.txt"))
if err != nil {
@ -89,8 +89,8 @@ func TestRepo_ForcePushRecovery(t *testing.T) {
if err != nil {
t.Fatalf("post-force-push Fetch: %v", err)
}
if !updated {
t.Error("post-force-push Fetch: expected updated=true")
if updated != gitshallow.UpdateChanged {
t.Error("post-force-push Fetch: expected changed")
}
got, err = os.ReadFile(filepath.Join(clone, "data.txt"))
if err != nil {

View File

@ -221,18 +221,27 @@ func (r *Repo) gc(ctx context.Context) error {
return err
}
// UpdateStatus describes what happened during an update.
type UpdateStatus uint8
const (
UpdateSkipped UpdateStatus = iota
UpdateCurrent
UpdateChanged
)
// Fetch clones the repo if missing, pulls otherwise, and conditionally runs
// GC based on GCInterval. Returns whether HEAD changed. Implements Upstream.
// Safe to call concurrently — concurrent callers share a single in-flight
// fetch (via singleflight) and all receive the same result. To force a
// pull regardless of MaxAge, set MaxAge=-1 before calling.
func (r *Repo) Update(ctx context.Context) (updated bool, err error) {
func (r *Repo) Update(ctx context.Context) (UpdateStatus, error) {
// MaxAge: file-mtime gate (FETCH_HEAD is rewritten on every successful
// fetch, so its mtime is "last time we talked to the remote").
if maxAge := r.effectiveMaxAge(); maxAge > 0 {
if info, err := os.Stat(filepath.Join(r.Path, ".git", "FETCH_HEAD")); err == nil {
if time.Since(info.ModTime()) < maxAge {
return false, nil
return UpdateSkipped, nil
}
}
}
@ -241,9 +250,12 @@ func (r *Repo) Update(ctx context.Context) (updated bool, err error) {
return r.fetch(ctx)
})
if err != nil {
return false, err
return UpdateCurrent, err
}
return v.(bool), nil
if v.(bool) {
return UpdateChanged, nil
}
return UpdateCurrent, nil
}
func (r *Repo) fetch(ctx context.Context) (bool, error) {

View File

@ -40,8 +40,8 @@ func TestRepo_Clone(t *testing.T) {
if err != nil {
t.Fatalf("Fetch (clone): %v", err)
}
if !updated {
t.Error("fresh clone: expected updated=true")
if updated != gitshallow.UpdateChanged {
t.Error("fresh clone: expected UpdateChanged")
}
for _, rel := range []string{
@ -73,8 +73,8 @@ func TestRepo_Pull_SameInstance(t *testing.T) {
if err != nil {
t.Fatalf("second Fetch: %v", err)
}
if updated {
t.Error("same-instance second pull: expected updated=false (already at HEAD)")
if updated == gitshallow.UpdateChanged {
t.Error("same-instance second pull: expected not UpdateChanged (already at HEAD)")
}
t.Log("same-instance pull correctly reported no update")
}
@ -94,8 +94,8 @@ func TestRepo_Pull_FreshInstance(t *testing.T) {
if err != nil {
t.Fatalf("fresh-instance Fetch: %v", err)
}
if updated {
t.Error("fresh-instance pull: expected updated=false (HEAD unchanged on disk)")
if updated == gitshallow.UpdateChanged {
t.Error("fresh-instance pull: expected not UpdateChanged (HEAD unchanged on disk)")
}
t.Log("fresh-instance pull correctly reported no update")
}

View File

@ -21,3 +21,8 @@ require (
golang.org/x/sys v0.38.0 // indirect
golang.org/x/tools v0.39.0 // indirect
)
replace (
github.com/therootcompany/golib/net/gitshallow => ../gitshallow
github.com/therootcompany/golib/sync/cacheable => ../../sync/cacheable
)

View File

@ -8,16 +8,12 @@ github.com/therootcompany/golib/io/transform/gsheet2csv v1.1.0 h1:R5zAPun5p3JPlN
github.com/therootcompany/golib/io/transform/gsheet2csv v1.1.0/go.mod h1:CsntOAhhIMdzSAYuwuRZtfkdoiER9+TfSgYOIDWN71o=
github.com/therootcompany/golib/net/dnsresolver v0.6.0 h1:sy08rzX/2QowofAKnCw1QtTDoatPL6/FXZcb+Wz4GlM=
github.com/therootcompany/golib/net/dnsresolver v0.6.0/go.mod h1:tOV99lX6ghpWPksxSFAt4CDe5XLefFzIGf2qXsZJap4=
github.com/therootcompany/golib/net/gitshallow v0.10.0 h1:cLN9ElTmNWk5HSbaRD3sSFuGdpgy4YXc+LVzjzT0nM4=
github.com/therootcompany/golib/net/gitshallow v0.10.0/go.mod h1:S/3OK7wMOqJJjP79U9Qw/l3jjEyrb/zsjJ8HAPAX/Fc=
github.com/therootcompany/golib/net/httpcache v0.6.0 h1:qFBnSs6rZrtUQ+fweP4bHF2FQd/Syvs7mRN5nO5YtkY=
github.com/therootcompany/golib/net/httpcache v0.6.0/go.mod h1:mKSQJWpGSCDBzWkTCwT7dO1SCVCsFmdkRgKr39fNS1U=
github.com/therootcompany/golib/net/ipcohort v0.9.0 h1:j3AaoW2u4XQJz1ya6fixv3pb3KgxIpZV8t0082/kS3E=
github.com/therootcompany/golib/net/ipcohort v0.9.0/go.mod h1:Ljcnt2xOryVDGIVV5Kzt5hj2LhW4lBAWo5OeeVYU9ys=
github.com/therootcompany/golib/net/iplist v0.9.2 h1:2WGFYzLhDyeGRuagLPe6Hp6gWMNqfqWpei9KG41C/9E=
github.com/therootcompany/golib/net/iplist v0.9.2/go.mod h1:TJ+0RP4M8/zkjdm+D9YXvDUJL2VZiC2VpnZJiK9Bfjk=
github.com/therootcompany/golib/sync/cacheable v0.1.0 h1:s6mz4dZv9Vsd0nuDz8IpS+n35GLjOwpjZlomimkqw1M=
github.com/therootcompany/golib/sync/cacheable v0.1.0/go.mod h1:0ZQ9sOtqyr1j8ZfSaAyPM1HlGi1B2qFgaGC4ULUbhD4=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=

View File

@ -216,7 +216,7 @@ func (ps *IPPrefixSet) Contains(addr netip.Addr) bool {
}
func (ps *IPPrefixSet) reload(ctx context.Context) (*ipcohort.Cohort, error) {
updated, err := ps.repo.Update(ctx)
status, err := ps.repo.Update(ctx)
if err != nil {
return nil, err
}
@ -226,7 +226,7 @@ func (ps *IPPrefixSet) reload(ctx context.Context) (*ipcohort.Cohort, error) {
paths[i] = ps.repo.FilePath(f)
}
if cachedCohortValid(updated, ps.cohort.Load(), paths) {
if cachedCohortValid(status, ps.cohort.Load(), paths) {
return nil, nil
}
@ -235,15 +235,14 @@ func (ps *IPPrefixSet) reload(ctx context.Context) (*ipcohort.Cohort, error) {
return nil, fmt.Errorf("load files: %w", err)
}
log().Info("prefix set loaded", "entries", commaify(cohort.Size()))
return cohort, nil
}
// cachedCohortValid reports whether the current cohort can be reused
// without reloading from disk: the repo was not updated, the cohort is
// non-nil and non-empty, and all data files are present.
func cachedCohortValid(updated bool, cohort *ipcohort.Cohort, paths []string) bool {
return !updated && cohort != nil && cohort.Size() > 0 && filesPresent(paths)
func cachedCohortValid(status gitshallow.UpdateStatus, cohort *ipcohort.Cohort, paths []string) bool {
return status != gitshallow.UpdateChanged && cohort != nil && cohort.Size() > 0 && filesPresent(paths)
}
func filesPresent(paths []string) bool {

View File

@ -6,6 +6,7 @@ import (
"path/filepath"
"testing"
"github.com/therootcompany/golib/net/gitshallow"
"github.com/therootcompany/golib/net/ipcohort"
)
@ -32,16 +33,16 @@ func TestCachedCohortValidRequiresFiles(t *testing.T) {
t.Fatal(err)
}
if !cachedCohortValid(false, cohort, []string{present}) {
if !cachedCohortValid(gitshallow.UpdateCurrent, cohort, []string{present}) {
t.Fatal("fresh metadata and present files should use the cache")
}
if cachedCohortValid(false, cohort, []string{present, filepath.Join(dir, "gone")}) {
if cachedCohortValid(gitshallow.UpdateCurrent, cohort, []string{present, filepath.Join(dir, "gone")}) {
t.Fatal("missing data file must invalidate the cache")
}
if cachedCohortValid(false, nil, []string{present}) {
if cachedCohortValid(gitshallow.UpdateCurrent, nil, []string{present}) {
t.Fatal("nil cohort must not be used")
}
if cachedCohortValid(true, cohort, []string{present}) {
if cachedCohortValid(gitshallow.UpdateChanged, cohort, []string{present}) {
t.Fatal("updated repo must reload")
}
}