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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ reaches your git server.
> Use **System Settings → Privacy & Security** and click **Open Anyway** for `dirtygit`, or right-click the
> binary in Finder and choose **Open** once.
>
> This will not be necessary if you install from source.
> This will not be necessary if you install from [#source](#source).

### Homebrew

Expand Down
4 changes: 2 additions & 2 deletions scanner/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func ScanWithProgress(config *Config, onProgress func(ScanProgress)) (*MultiGitS
log.Printf("branch status scan failed for %s: %v", d, err)
}

if !st.IsClean() || branches.HasLocalRemoteMismatch() {
if !st.IsClean() || branches.HasLocalRemoteMismatchRespectingConfig(config) {
atomic.AddInt64(&totalStatusDuration, duration.Nanoseconds())
results.AddResult(d, RepoStatus{
Status: st,
Expand Down Expand Up @@ -148,6 +148,6 @@ func StatusForRepo(config *Config, dir string) (RepoStatus, bool, error) {
Branches: branches,
ScanTime: time.Since(start),
}
include := !st.IsClean() || branches.HasLocalRemoteMismatch()
include := !st.IsClean() || branches.HasLocalRemoteMismatchRespectingConfig(config)
return rs, include, nil
}
40 changes: 40 additions & 0 deletions scanner/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package scanner

import (
"fmt"
"log"
"os"
"regexp"
"strings"
Expand Down Expand Up @@ -149,6 +150,45 @@ 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
}
}
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
}

// LocalRemoteMismatchReasons returns a short line explaining why
// [BranchStatus.HasLocalRemoteMismatch] is true, or nil when that predicate is false.
func (b BranchStatus) LocalRemoteMismatchReasons() []string {
Expand Down
105 changes: 105 additions & 0 deletions scanner/types_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package scanner

import (
"os"
"path/filepath"
"reflect"
"testing"
)
Expand Down Expand Up @@ -162,6 +164,109 @@ 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
34 changes: 17 additions & 17 deletions ui/status_layout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,14 +429,17 @@ branches:
}
}

func TestRefreshBranchContentAlwaysListsDefaultBranchesInSync(t *testing.T) {
func TestRefreshBranchContentDefaultBranchOverridesLocalOnlyHide(t *testing.T) {
tmp := t.TempDir()
cfgPath := filepath.Join(tmp, "dirtygit-defaults.yml")
cfgPath := filepath.Join(tmp, "dirtygit-default-hide.yml")
content := `
scandirs:
include:
- /tmp
branches:
hidelocalonly:
regex:
- "^main$"
default:
- main
`
Expand All @@ -458,21 +461,15 @@ branches:
Locations: []scanner.BranchLocation{
{Name: "local", Exists: true, TipHash: "aaaaaaaaaaaaaaaa", TipUnix: 1_700_000_000, UniqueCount: 2},
{Name: "origin", Exists: true, TipHash: "bbbbbbbbbbbbbbbb", TipUnix: 1_700_000_001, UniqueCount: 1, NewestUniqueUnix: 1_700_000_001, Incoming: 1, Outgoing: 2},
{Name: "upstream", Exists: false},
},
LocalBranches: []scanner.LocalBranchRef{
{Name: "aaa", TipHash: "aaaaaaaaaaaaaaaa", TipUnix: 1_700_000_000, Current: true, Locations: []scanner.BranchLocation{
{Name: "local", Exists: true, TipHash: "aaaaaaaaaaaaaaaa", TipUnix: 1_700_000_000, UniqueCount: 2},
{Name: "origin", Exists: true, TipHash: "bbbbbbbbbbbbbbbb", TipUnix: 1_700_000_001, UniqueCount: 1, NewestUniqueUnix: 1_700_000_001, Incoming: 1, Outgoing: 2},
{Name: "upstream", Exists: false},
}},
{Name: "main", TipHash: "cccccccccccccccc", TipUnix: 1_700_000_002, Current: false, Locations: []scanner.BranchLocation{
{Name: "local", Exists: true, TipHash: "cccccccccccccccc", TipUnix: 1_700_000_002},
{Name: "origin", Exists: true, TipHash: "cccccccccccccccc", TipUnix: 1_700_000_002},
}},
{Name: "zzz", TipHash: "dddddddddddddddd", TipUnix: 1_700_000_003, Current: false, Locations: []scanner.BranchLocation{
{Name: "local", Exists: true, TipHash: "dddddddddddddddd", TipUnix: 1_700_000_003},
{Name: "origin", Exists: true, TipHash: "dddddddddddddddd", TipUnix: 1_700_000_003},
{Name: "origin", Exists: false},
}},
},
},
Expand All @@ -481,7 +478,7 @@ branches:
m.refreshBranchContent(60)
rows := m.branchTable.Rows()
if len(rows) != 2 {
t.Fatalf("branch rows len = %d, want 2 (zzz in sync omitted, main listed as default)", len(rows))
t.Fatalf("branch rows len = %d, want 2 (main shown despite hide regex)", len(rows))
}
got := map[string]bool{rows[0][0]: true, rows[1][0]: true}
for _, name := range []string{"aaa", "main"} {
Expand All @@ -491,17 +488,14 @@ branches:
}
}

func TestRefreshBranchContentDefaultBranchOverridesLocalOnlyHide(t *testing.T) {
func TestRefreshBranchContentAlwaysListsDefaultBranchesInSync(t *testing.T) {
tmp := t.TempDir()
cfgPath := filepath.Join(tmp, "dirtygit-default-hide.yml")
cfgPath := filepath.Join(tmp, "dirtygit-defaults.yml")
content := `
scandirs:
include:
- /tmp
branches:
hidelocalonly:
regex:
- "^main$"
default:
- main
`
Expand All @@ -523,15 +517,21 @@ branches:
Locations: []scanner.BranchLocation{
{Name: "local", Exists: true, TipHash: "aaaaaaaaaaaaaaaa", TipUnix: 1_700_000_000, UniqueCount: 2},
{Name: "origin", Exists: true, TipHash: "bbbbbbbbbbbbbbbb", TipUnix: 1_700_000_001, UniqueCount: 1, NewestUniqueUnix: 1_700_000_001, Incoming: 1, Outgoing: 2},
{Name: "upstream", Exists: false},
},
LocalBranches: []scanner.LocalBranchRef{
{Name: "aaa", TipHash: "aaaaaaaaaaaaaaaa", TipUnix: 1_700_000_000, Current: true, Locations: []scanner.BranchLocation{
{Name: "local", Exists: true, TipHash: "aaaaaaaaaaaaaaaa", TipUnix: 1_700_000_000, UniqueCount: 2},
{Name: "origin", Exists: true, TipHash: "bbbbbbbbbbbbbbbb", TipUnix: 1_700_000_001, UniqueCount: 1, NewestUniqueUnix: 1_700_000_001, Incoming: 1, Outgoing: 2},
{Name: "upstream", Exists: false},
}},
{Name: "main", TipHash: "cccccccccccccccc", TipUnix: 1_700_000_002, Current: false, Locations: []scanner.BranchLocation{
{Name: "local", Exists: true, TipHash: "cccccccccccccccc", TipUnix: 1_700_000_002},
{Name: "origin", Exists: false},
{Name: "origin", Exists: true, TipHash: "cccccccccccccccc", TipUnix: 1_700_000_002},
}},
{Name: "zzz", TipHash: "dddddddddddddddd", TipUnix: 1_700_000_003, Current: false, Locations: []scanner.BranchLocation{
{Name: "local", Exists: true, TipHash: "dddddddddddddddd", TipUnix: 1_700_000_003},
{Name: "origin", Exists: true, TipHash: "dddddddddddddddd", TipUnix: 1_700_000_003},
}},
},
},
Expand All @@ -540,7 +540,7 @@ branches:
m.refreshBranchContent(60)
rows := m.branchTable.Rows()
if len(rows) != 2 {
t.Fatalf("branch rows len = %d, want 2 (main shown despite hide regex)", len(rows))
t.Fatalf("branch rows len = %d, want 2 (zzz in sync omitted, main listed as default)", len(rows))
}
got := map[string]bool{rows[0][0]: true, rows[1][0]: true}
for _, name := range []string{"aaa", "main"} {
Expand Down
Loading