diff --git a/scanner/find.go b/scanner/find.go index 33caf73..ec25271 100644 --- a/scanner/find.go +++ b/scanner/find.go @@ -77,6 +77,7 @@ func walkone(ctx context.Context, dir string, config *Config, results chan strin return metaErr } if ok { + log.Printf("git %s", path) repo := filepath.Dir(path) if onRepoFound != nil { onRepoFound(repo) diff --git a/scanner/scan.go b/scanner/scan.go index 8335e01..da8a68e 100644 --- a/scanner/scan.go +++ b/scanner/scan.go @@ -60,7 +60,6 @@ func ScanWithProgress(config *Config, onProgress func(ScanProgress)) (*MultiGitS var eg errgroup.Group for d := range repositories { - d := d // copy loop variable eg.Go(func() error { // About to run git status for this repo; set CurrentPath so the scan modal // shows which directory is active (and keeps showing it through the rest @@ -97,8 +96,8 @@ func ScanWithProgress(config *Config, onProgress func(ScanProgress)) (*MultiGitS if err != nil { log.Printf("branch status scan failed for %s: %v", d, err) } - - if !st.IsClean() || branches.HasLocalRemoteMismatchRespectingConfig(config) { + branches.FilterLocalOnlyForConfig(config) + if !st.IsClean() || branches.HasLocalRemoteMismatch() { atomic.AddInt64(&totalStatusDuration, duration.Nanoseconds()) results.AddResult(d, RepoStatus{ Status: st, @@ -141,6 +140,7 @@ func StatusForRepo(config *Config, dir string) (RepoStatus, bool, error) { if berr != nil { log.Printf("branch status scan failed for %s: %v", dir, berr) } + branches.FilterLocalOnlyForConfig(config) rs := RepoStatus{ Status: st, @@ -148,6 +148,6 @@ func StatusForRepo(config *Config, dir string) (RepoStatus, bool, error) { Branches: branches, ScanTime: time.Since(start), } - include := !st.IsClean() || branches.HasLocalRemoteMismatchRespectingConfig(config) + include := !st.IsClean() || branches.HasLocalRemoteMismatch() return rs, include, nil } diff --git a/scanner/types.go b/scanner/types.go index 17cea52..283b57a 100644 --- a/scanner/types.go +++ b/scanner/types.go @@ -2,7 +2,6 @@ package scanner import ( "fmt" - "log" "os" "regexp" "strings" @@ -113,7 +112,7 @@ type BranchLocation struct { // HasLocalRemoteMismatch reports whether the current local branch differs from // any tracked remote location for the same branch name. A clean repo that is // only behind the remote (incoming commits, nothing to push) is not a mismatch. -func (b BranchStatus) HasLocalRemoteMismatch() bool { +func (b *BranchStatus) HasLocalRemoteMismatch() bool { if b.Detached { return false } @@ -150,48 +149,26 @@ func (b BranchStatus) HasLocalRemoteMismatch() bool { return hasRemote && local.UniqueCount > 0 } -// currentLocalRefForHidePolicy returns the checked-out branch as a [LocalBranchRef] -// for [Config.ShouldHideLocalOnlyBranch], or false when detached or indeterminate. -func (b BranchStatus) currentLocalRefForHidePolicy() (LocalBranchRef, bool) { - if b.Detached { - return LocalBranchRef{}, false - } - for i := range b.LocalBranches { - if b.LocalBranches[i].Current { - return b.LocalBranches[i], true +// FilterLocalOnlyForConfig filters out local-only branches that +// [Config.ShouldHideLocalOnlyBranch] matches unless [Config.AlwaysListBranch] applies. +func (b *BranchStatus) FilterLocalOnlyForConfig(c *Config) { + refs := b.LocalBranches + if c == nil || len(refs) == 0 { + return + } + out := make([]LocalBranchRef, 0) + for _, lb := range refs { + if c.ShouldHideLocalOnlyBranch(lb) && !c.AlwaysListBranch(lb.Name) { + continue } + out = append(out, lb) } - if b.Branch != "" && len(b.Locations) > 0 { - return LocalBranchRef{Name: b.Branch, Locations: b.Locations}, true - } - return LocalBranchRef{}, false -} - -// HasLocalRemoteMismatchRespectingConfig is like [BranchStatus.HasLocalRemoteMismatch] -// but returns false when the only effective concern is a local-only current branch -// that [Config.ShouldHideLocalOnlyBranch] would filter out. Branches listed under -// branches.default still count as a mismatch (same as the branch pane). -func (b BranchStatus) HasLocalRemoteMismatchRespectingConfig(c *Config) bool { - if !b.HasLocalRemoteMismatch() { - return false - } - if c == nil { - return true - } - lb, ok := b.currentLocalRefForHidePolicy() - if !ok { - return true - } - if c.ShouldHideLocalOnlyBranch(lb) && !c.AlwaysListBranch(lb.Name) { - log.Println("hiding local-only branch", lb.Name) - return false - } - return true + b.LocalBranches = out } // LocalRemoteMismatchReasons returns a short line explaining why // [BranchStatus.HasLocalRemoteMismatch] is true, or nil when that predicate is false. -func (b BranchStatus) LocalRemoteMismatchReasons() []string { +func (b *BranchStatus) LocalRemoteMismatchReasons() []string { if b.Detached { return nil } diff --git a/scanner/types_test.go b/scanner/types_test.go index 63abc8d..d586aa2 100644 --- a/scanner/types_test.go +++ b/scanner/types_test.go @@ -1,8 +1,6 @@ package scanner import ( - "os" - "path/filepath" "reflect" "testing" ) @@ -164,109 +162,6 @@ func TestBranchStatusHasLocalRemoteMismatch(t *testing.T) { } } -func TestBranchStatusHasLocalRemoteMismatchRespectingConfig(t *testing.T) { - t.Parallel() - - tmp := t.TempDir() - cfgPath := filepath.Join(tmp, "cfg.yml") - content := ` -scandirs: - include: - - /x -branches: - hidelocalonly: - regex: - - "^wip/" - default: - - wip/release -` - if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil { - t.Fatal(err) - } - cfg, err := ParseConfigFile(cfgPath, "") - if err != nil { - t.Fatal(err) - } - - localOnlyLocs := []BranchLocation{ - {Name: "local", Exists: true, TipHash: "aaa"}, - {Name: "origin", Exists: false}, - } - - t.Run("nil config unchanged from HasLocalRemoteMismatch", func(t *testing.T) { - t.Parallel() - bs := BranchStatus{ - Branch: "wip/foo", - Locations: localOnlyLocs, - LocalBranches: []LocalBranchRef{ - {Name: "wip/foo", Current: true, Locations: localOnlyLocs}, - }, - } - if !bs.HasLocalRemoteMismatch() { - t.Fatal("sanity: raw mismatch expected") - } - if !bs.HasLocalRemoteMismatchRespectingConfig(nil) { - t.Fatal("nil config should keep mismatch visible") - } - }) - - t.Run("hide local-only wip branch suppresses mismatch", func(t *testing.T) { - t.Parallel() - bs := BranchStatus{ - Branch: "wip/foo", - Locations: localOnlyLocs, - LocalBranches: []LocalBranchRef{ - {Name: "wip/foo", Current: true, Locations: localOnlyLocs}, - }, - } - if bs.HasLocalRemoteMismatchRespectingConfig(cfg) { - t.Fatal("expected hidden local-only branch not to count as mismatch for scan") - } - }) - - t.Run("main branch still mismatches with hide rules", func(t *testing.T) { - t.Parallel() - bs := BranchStatus{ - Branch: "main", - Locations: localOnlyLocs, - LocalBranches: []LocalBranchRef{ - {Name: "main", Current: true, Locations: localOnlyLocs}, - }, - } - if !bs.HasLocalRemoteMismatchRespectingConfig(cfg) { - t.Fatal("expected main to still count as mismatch") - } - }) - - t.Run("branches default overrides hide for scan", func(t *testing.T) { - t.Parallel() - bs := BranchStatus{ - Branch: "wip/release", - Locations: localOnlyLocs, - LocalBranches: []LocalBranchRef{ - {Name: "wip/release", Current: true, Locations: localOnlyLocs}, - }, - } - if !bs.HasLocalRemoteMismatchRespectingConfig(cfg) { - t.Fatal("wip/release is under branches.default and must still count") - } - }) - - t.Run("synthetic current ref when LocalBranches empty", func(t *testing.T) { - t.Parallel() - bs := BranchStatus{ - Branch: "wip/foo", - Detached: false, - Locations: localOnlyLocs, - LocalBranches: nil, - NewestLocation: "local", - } - if bs.HasLocalRemoteMismatchRespectingConfig(cfg) { - t.Fatal("expected synthetic current ref to honor hide policy") - } - }) -} - func TestLocalBranchRefHasTipMismatchAcrossRemotes(t *testing.T) { t.Parallel()