Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scanner/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions scanner/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -141,13 +140,14 @@ 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,
Porcelain: porcelain,
Branches: branches,
ScanTime: time.Since(start),
}
include := !st.IsClean() || branches.HasLocalRemoteMismatchRespectingConfig(config)
include := !st.IsClean() || branches.HasLocalRemoteMismatch()
return rs, include, nil
}
53 changes: 15 additions & 38 deletions scanner/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package scanner

import (
"fmt"
"log"
"os"
"regexp"
"strings"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
105 changes: 0 additions & 105 deletions scanner/types_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package scanner

import (
"os"
"path/filepath"
"reflect"
"testing"
)
Expand Down Expand Up @@ -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()

Expand Down
Loading