From 385fe5dc0e86f4f14f1f1138937734884fc01d4d Mon Sep 17 00:00:00 2001 From: Matt Vinall Date: Wed, 13 May 2026 20:01:58 +0100 Subject: [PATCH] wip --- scanner/branch_status.go | 57 ++++++++++++++---- scanner/inclusion_test.go | 22 ++++--- scanner/scan.go | 10 ++-- scanner/types.go | 123 +++++++++++++++++++++++++++++--------- scanner/types_test.go | 75 ++++++++++++++--------- ui/status_layout.go | 18 +++--- ui/status_layout_test.go | 18 ------ ui/update.go | 2 +- ui/view_test.go | 13 ++-- 9 files changed, 228 insertions(+), 110 deletions(-) diff --git a/scanner/branch_status.go b/scanner/branch_status.go index b6a2feb..d83bb80 100644 --- a/scanner/branch_status.go +++ b/scanner/branch_status.go @@ -212,6 +212,15 @@ func computeBranchLocations(d, branchName string, remotes []string) ([]BranchLoc return locations, nil } +func tipFromLocalBranchLocation(locations []BranchLocation) (hash string, unix int64) { + for _, loc := range locations { + if loc.Name == "local" && loc.Exists { + return loc.TipHash, loc.TipUnix + } + } + return "", 0 +} + func GitBranchStatus(d string) (BranchStatus, error) { branch, detached, err := currentBranch(d) if err != nil { @@ -240,38 +249,64 @@ func GitBranchStatus(d string) (BranchStatus, error) { if err != nil { return BranchStatus{}, err } + tipHash, tipUnix := tipFromLocalBranchLocation(locations) return BranchStatus{ - Branch: branch, - Detached: false, - Locations: locations, - LocalBranches: nil, + Branch: branch, + Detached: false, + LocalBranches: []LocalBranchRef{{ + Name: branch, + TipHash: tipHash, + TipUnix: tipUnix, + Current: true, + Locations: locations, + }}, }, nil } - var topLocations []BranchLocation for i := range locals { locs, err := computeBranchLocations(d, locals[i].Name, remotes) if err != nil { return BranchStatus{}, err } locals[i].Locations = locs + } + + var foundCurrent bool + for i := range locals { if locals[i].Current { - topLocations = locs + foundCurrent = true + break } } - - if topLocations == nil { - var err2 error - topLocations, err2 = computeBranchLocations(d, branch, remotes) + if !foundCurrent { + locs, err2 := computeBranchLocations(d, branch, remotes) if err2 != nil { return BranchStatus{}, err2 } + matched := false + for i := range locals { + if locals[i].Name == branch { + locals[i].Locations = locs + locals[i].Current = true + matched = true + break + } + } + if !matched { + tipHash, tipUnix := tipFromLocalBranchLocation(locs) + locals = append([]LocalBranchRef{{ + Name: branch, + TipHash: tipHash, + TipUnix: tipUnix, + Current: true, + Locations: locs, + }}, locals...) + } } return BranchStatus{ Branch: branch, Detached: false, - Locations: topLocations, LocalBranches: locals, }, nil } diff --git a/scanner/inclusion_test.go b/scanner/inclusion_test.go index 14f471b..7d1e09f 100644 --- a/scanner/inclusion_test.go +++ b/scanner/inclusion_test.go @@ -19,10 +19,13 @@ func TestRepoInclusionReasons_uncommitted(t *testing.T) { Path: g, Staging: 'M', Worktree: 'M', }}, }, - Branches: BranchStatus{Branch: "main", Locations: []BranchLocation{ - {Name: "local", Exists: true, TipHash: "a"}, - {Name: "origin", Exists: true, TipHash: "a"}, - }}, + Branches: BranchStatus{Branch: "main", LocalBranches: []LocalBranchRef{{ + Name: "main", Current: true, + Locations: []BranchLocation{ + {Name: "local", Exists: true, TipHash: "a"}, + {Name: "origin", Exists: true, TipHash: "a"}, + }, + }}}, } lines := RepoInclusionReasons(rs) if len(lines) < 1 || !strings.Contains(lines[0], "Uncommitted") { @@ -35,10 +38,13 @@ func TestRepoInclusionReasons_branchOnly(t *testing.T) { rs := RepoStatus{ Branches: BranchStatus{ Branch: "main", - Locations: []BranchLocation{ - {Name: "local", Exists: true, TipHash: "aaa"}, - {Name: "origin", Exists: true, TipHash: "bbb", Incoming: 0, Outgoing: 1}, - }, + LocalBranches: []LocalBranchRef{{ + Name: "main", Current: true, + Locations: []BranchLocation{ + {Name: "local", Exists: true, TipHash: "aaa"}, + {Name: "origin", Exists: true, TipHash: "bbb", Incoming: 0, Outgoing: 1}, + }, + }}, }, } lines := RepoInclusionReasons(rs) diff --git a/scanner/scan.go b/scanner/scan.go index a8c0ce8..e9c5063 100644 --- a/scanner/scan.go +++ b/scanner/scan.go @@ -56,9 +56,8 @@ func ScanWithProgress(config *Config, onProgress func(ScanProgress)) (*MultiGitS for d := range repositories { 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 - // of this iteration via the update after GitStatus returns). + // About to run StatusForRepo for this path; set CurrentPath so the scan modal + // shows which directory is active until this worker finishes and the UI updates. reportProgress(onProgress, ScanProgress{ ReposFound: int(found.Load()), ReposChecked: int(checked.Load()), @@ -72,9 +71,8 @@ func ScanWithProgress(config *Config, onProgress func(ScanProgress)) (*MultiGitS return err } n := checked.Add(1) - // Git status finished for this repo; advance ReposChecked and retain - // CurrentPath until the next channel receive so the path line does not - // flicker to empty while GitBranchStatus and filtering still run. + // Per-repo StatusForRepo finished; advance ReposChecked and retain + // CurrentPath until the next progress event so the path line does not flicker. reportProgress(onProgress, ScanProgress{ ReposFound: int(found.Load()), ReposChecked: int(n), diff --git a/scanner/types.go b/scanner/types.go index ebf12dd..16eb519 100644 --- a/scanner/types.go +++ b/scanner/types.go @@ -9,6 +9,10 @@ import ( "github.com/go-git/go-git/v5" ) +// RepoStatus aggregates one repository's working tree and branch metadata: +// parsed git status --porcelain (Porcelain), HEAD and local-branch layout with +// remote tips (Branches), and an embedded [git.Status] rebuilt from Porcelain +// for code that expects go-git's map form. type RepoStatus struct { git.Status @@ -16,10 +20,14 @@ type RepoStatus struct { Branches BranchStatus } +// BranchStatus is HEAD identity plus an ordered list of local branch refs and +// per-branch local vs same-named remote comparison rows. type BranchStatus struct { - Branch string - Detached bool - Locations []BranchLocation + // Branch is the checked-out branch short name, or when Detached the short + // HEAD object name (see git rev-parse --short HEAD). + Branch string + // Detached is true when HEAD is not on a branch (detached HEAD). + Detached bool // LocalBranches lists refs/heads in name order (from git for-each-ref). LocalBranches []LocalBranchRef } @@ -28,10 +36,16 @@ type BranchStatus struct { // Locations holds local vs same-named remote refs (refs/remotes//); // it is empty when detached or before GitBranchStatus fills it. type LocalBranchRef struct { - Name string - TipHash string - TipUnix int64 - Current bool + // Name is the short branch name (the refs/heads/* ref without the prefix). + Name string + // TipHash is the full object name of the local ref tip; TipUnix is the tip + // commit's committer date in Unix seconds (from branch listing / git show). + TipHash string + TipUnix int64 + // Current is true when this row is the checked-out branch. + Current bool + // Locations compares this local branch to same-named refs on each configured + // remote; see [BranchLocation]. Empty when detached or before branch scan fills it. Locations []BranchLocation } @@ -88,11 +102,29 @@ func (lb LocalBranchRef) IsLocalOnly() bool { return true } +// BranchLocation is one side of a local branch compared to same-named remotes: +// either the local ref (Name "local") or a configured remote's +// refs/remotes//. Populated by branch status scanning; stored in +// [LocalBranchRef.Locations]. The UI and helpers use it for tip hashes, +// ahead/behind counts (Incoming/Outgoing vs local), and mismatch detection. type BranchLocation struct { - Name string - Exists bool - TipHash string - TipUnix int64 + // Either "local" or the name of a configured remote. + Name string + + // Exists is true when this location's ref (refs/heads/ for "local", + // refs/remotes// otherwise) exists and resolves to a commit; + // false when the ref is missing. + Exists bool + + // TipHash is the full hex object name of this ref's tip commit when Exists; + // empty when not Exists. + TipHash string + // TipUnix is the tip commit's committer date in Unix seconds (from git show); + // zero when not Exists. + TipUnix int64 + // UniqueCount is commits reachable from this ref but not from any other + // Exists location for the same branch (local plus each remote in the scan). + // Zero when not Exists. UniqueCount int // Incoming/Outgoing compare this ref to the local branch ref only (remote // rows). Incoming is commits reachable from this remote but not local (+N); @@ -104,6 +136,21 @@ type BranchLocation struct { HistoriesUnrelated bool } +// CurrentBranchLocations returns local vs same-named remote rows for the +// checked-out branch (the [LocalBranchRef] with Current: true). Returns nil when +// detached or when no current row exists. +func (b *BranchStatus) CurrentBranchLocations() []BranchLocation { + if b == nil || b.Detached { + return nil + } + for i := range b.LocalBranches { + if b.LocalBranches[i].Current { + return b.LocalBranches[i].Locations + } + } + return nil +} + // 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. @@ -111,11 +158,15 @@ func (b *BranchStatus) HasLocalRemoteMismatch() bool { if b.Detached { return false } + locs := b.CurrentBranchLocations() + if len(locs) == 0 { + return false + } var local *BranchLocation - for i := range b.Locations { - if b.Locations[i].Name == "local" { - local = &b.Locations[i] + for i := range locs { + if locs[i].Name == "local" { + local = &locs[i] break } } @@ -124,8 +175,8 @@ func (b *BranchStatus) HasLocalRemoteMismatch() bool { } hasRemote := false - for i := range b.Locations { - loc := b.Locations[i] + for i := range locs { + loc := locs[i] if loc.Name == "local" { continue } @@ -146,6 +197,7 @@ func (b *BranchStatus) HasLocalRemoteMismatch() bool { // FilterLocalOnlyForConfig filters out local-only branches that // [Config.ShouldHideLocalOnlyBranch] matches unless [Config.AlwaysListBranch] applies. +// The checked-out branch is never removed so HEAD remote comparison stays available. func (b *BranchStatus) FilterLocalOnlyForConfig(c *Config) { refs := b.LocalBranches if c == nil || len(refs) == 0 { @@ -153,6 +205,10 @@ func (b *BranchStatus) FilterLocalOnlyForConfig(c *Config) { } out := make([]LocalBranchRef, 0) for _, lb := range refs { + if lb.Current { + out = append(out, lb) + continue + } if c.ShouldHideLocalOnlyBranch(lb) && !c.AlwaysListBranch(lb.Name) { continue } @@ -167,10 +223,14 @@ func (b *BranchStatus) LocalRemoteMismatchReasons() []string { if b.Detached { return nil } + locs := b.CurrentBranchLocations() + if len(locs) == 0 { + return nil + } var local *BranchLocation - for i := range b.Locations { - if b.Locations[i].Name == "local" { - local = &b.Locations[i] + for i := range locs { + if locs[i].Name == "local" { + local = &locs[i] break } } @@ -182,8 +242,8 @@ func (b *BranchStatus) LocalRemoteMismatchReasons() []string { branchName = "current branch" } hasRemote := false - for i := range b.Locations { - loc := b.Locations[i] + for i := range locs { + loc := locs[i] if loc.Name == "local" { continue } @@ -216,13 +276,18 @@ func (b *BranchStatus) LocalRemoteMismatchReasons() []string { return nil } +// PorcelainEntry is one parsed line of git status --porcelain (short format). type PorcelainEntry struct { - Staging git.StatusCode - Worktree git.StatusCode - Path string + // Staging and Worktree are the two status columns (index vs working tree). + Staging git.StatusCode + Worktree git.StatusCode + // Path is the file path, or the new path for a rename. + Path string + // OriginalPath is the old path for a rename; empty when not a rename. OriginalPath string } +// PorcelainStatus is the full porcelain parse for one repo (entry order matches git output). type PorcelainStatus struct { Entries []PorcelainEntry } @@ -238,12 +303,16 @@ func (p PorcelainStatus) ToGitStatus() git.Status { return st } -// ScanProgress reports coarse scan activity for UIs (discovery vs git status). +// ScanProgress reports coarse scan activity for UIs (discovery vs per-repo work). // ReposFound may increase while ReposChecked catches up; both match the final total when complete. type ScanProgress struct { - ReposFound int + // ReposFound is how many git repositories have been discovered so far. + ReposFound int + // ReposChecked is how many of those have finished StatusForRepo (porcelain, + // branch metadata, and config filtering), not git status alone. ReposChecked int - CurrentPath string + // CurrentPath is the path currently being processed (for status display). + CurrentPath string } type Config struct { diff --git a/scanner/types_test.go b/scanner/types_test.go index d586aa2..c48761d 100644 --- a/scanner/types_test.go +++ b/scanner/types_test.go @@ -78,59 +78,77 @@ func TestBranchStatusHasLocalRemoteMismatch(t *testing.T) { { name: "no remotes", in: BranchStatus{ - Locations: []BranchLocation{ - {Name: "local", Exists: true, TipHash: "abc"}, - }, + LocalBranches: []LocalBranchRef{{ + Current: true, + Locations: []BranchLocation{ + {Name: "local", Exists: true, TipHash: "abc"}, + }, + }}, }, want: false, }, { name: "matching local and remote", in: BranchStatus{ - Locations: []BranchLocation{ - {Name: "local", Exists: true, TipHash: "abc"}, - {Name: "origin", Exists: true, TipHash: "abc"}, - }, + LocalBranches: []LocalBranchRef{{ + Current: true, + Locations: []BranchLocation{ + {Name: "local", Exists: true, TipHash: "abc"}, + {Name: "origin", Exists: true, TipHash: "abc"}, + }, + }}, }, want: false, }, { name: "local ahead of remote", in: BranchStatus{ - Locations: []BranchLocation{ - {Name: "local", Exists: true, TipHash: "aaa"}, - {Name: "origin", Exists: true, TipHash: "bbb", Incoming: 0, Outgoing: 1}, - }, + LocalBranches: []LocalBranchRef{{ + Current: true, + Locations: []BranchLocation{ + {Name: "local", Exists: true, TipHash: "aaa"}, + {Name: "origin", Exists: true, TipHash: "bbb", Incoming: 0, Outgoing: 1}, + }, + }}, }, want: true, }, { name: "remote ahead of local (behind)", in: BranchStatus{ - Locations: []BranchLocation{ - {Name: "local", Exists: true, TipHash: "aaa"}, - {Name: "origin", Exists: true, TipHash: "bbb", Incoming: 2, Outgoing: 0}, - }, + LocalBranches: []LocalBranchRef{{ + Current: true, + Locations: []BranchLocation{ + {Name: "local", Exists: true, TipHash: "aaa"}, + {Name: "origin", Exists: true, TipHash: "bbb", Incoming: 2, Outgoing: 0}, + }, + }}, }, want: false, }, { name: "remote ahead but unrelated histories", in: BranchStatus{ - Locations: []BranchLocation{ - {Name: "local", Exists: true, TipHash: "aaa"}, - {Name: "origin", Exists: true, TipHash: "bbb", HistoriesUnrelated: true, Incoming: 1, Outgoing: 0}, - }, + LocalBranches: []LocalBranchRef{{ + Current: true, + Locations: []BranchLocation{ + {Name: "local", Exists: true, TipHash: "aaa"}, + {Name: "origin", Exists: true, TipHash: "bbb", HistoriesUnrelated: true, Incoming: 1, Outgoing: 0}, + }, + }}, }, want: true, }, { name: "remote branch missing", in: BranchStatus{ - Locations: []BranchLocation{ - {Name: "local", Exists: true, TipHash: "aaa"}, - {Name: "origin", Exists: false}, - }, + LocalBranches: []LocalBranchRef{{ + Current: true, + Locations: []BranchLocation{ + {Name: "local", Exists: true, TipHash: "aaa"}, + {Name: "origin", Exists: false}, + }, + }}, }, want: true, }, @@ -138,10 +156,13 @@ func TestBranchStatusHasLocalRemoteMismatch(t *testing.T) { name: "tips match but local has unique-only commits", in: BranchStatus{ Branch: "main", - Locations: []BranchLocation{ - {Name: "local", Exists: true, TipHash: "abc", UniqueCount: 2}, - {Name: "origin", Exists: true, TipHash: "abc"}, - }, + LocalBranches: []LocalBranchRef{{ + Current: true, + Locations: []BranchLocation{ + {Name: "local", Exists: true, TipHash: "abc", UniqueCount: 2}, + {Name: "origin", Exists: true, TipHash: "abc"}, + }, + }}, }, want: true, }, diff --git a/ui/status_layout.go b/ui/status_layout.go index bc38219..a0016b1 100644 --- a/ui/status_layout.go +++ b/ui/status_layout.go @@ -282,7 +282,7 @@ func statusColumns(totalWidth int) []table.Column { } } -// branchRowColumns sizes the branch pane: one row per local branch name. +// branchRowColumns sizes the branch pane columns for branch summary rows. // Account for Padding(0, 1) on every header and cell (see statusColumns). func branchRowColumns(totalWidth int) []table.Column { const cols = 4 @@ -306,13 +306,16 @@ func branchRowColumns(totalWidth int) []table.Column { } } -// branchRemoteSummary compresses local vs remote tips for the checked-out branch -// when BranchStatus.Locations is populated (e.g. no local heads listed). +// branchRemoteSummary compresses local vs remote tips for the checked-out branch. func branchRemoteSummary(b scanner.BranchStatus) string { - if b.Detached || len(b.Locations) == 0 { + if b.Detached { return "-" } - return branchRemoteSummaryFromLocations(b.Locations) + locs := b.CurrentBranchLocations() + if len(locs) == 0 { + return "-" + } + return branchRemoteSummaryFromLocations(locs) } func branchRemoteSummaryFromLocations(locations []scanner.BranchLocation) string { @@ -375,7 +378,8 @@ func sortLocalBranchesByTipNewestFirst(branches []scanner.LocalBranchRef) { }) } -// refreshBranchContent rebuilds the branch pane: one table row per local branch name. +// refreshBranchContent rebuilds the branch pane: one table row per local branch +// that the pane lists (tip mismatch vs remotes, local-only hide rules, and defaults). func (m *model) refreshBranchContent(totalWidth int) { cols := branchRowColumns(totalWidth) m.branchTable.SetColumns(cols) @@ -415,7 +419,7 @@ func (m *model) refreshBranchContent(totalWidth int) { remote := branchRemoteSummary(branch) tip := "-" when := "-" - for _, loc := range branch.Locations { + for _, loc := range branch.CurrentBranchLocations() { if loc.Name == "local" && loc.Exists { tip = shortHash(loc.TipHash) when = relativeTime(loc.TipUnix) diff --git a/ui/status_layout_test.go b/ui/status_layout_test.go index b426163..116af49 100644 --- a/ui/status_layout_test.go +++ b/ui/status_layout_test.go @@ -330,11 +330,6 @@ func TestRefreshBranchContentOneRowPerBranch(t *testing.T) { m.repositories.Set("/repo", scanner.RepoStatus{ Branches: scanner.BranchStatus{ Branch: "aaa", - 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, Incoming: 1, Outgoing: 2}, - {Name: "upstream", Exists: false}, - }, // Names sort opposite to recency so the test proves UI order is by tip time, not name. LocalBranches: []scanner.LocalBranchRef{ {Name: "aaa", TipHash: "aaaaaaaaaaaaaaaa", TipUnix: 1_700_000_000, Current: true, Locations: []scanner.BranchLocation{ @@ -394,10 +389,6 @@ branches: m.repositories.Set("/repo", scanner.RepoStatus{ Branches: scanner.BranchStatus{ Branch: "aaa", - 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, Incoming: 1, Outgoing: 2}, - }, 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}, @@ -456,10 +447,6 @@ branches: m.repositories.Set("/repo", scanner.RepoStatus{ Branches: scanner.BranchStatus{ Branch: "aaa", - 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, Incoming: 1, Outgoing: 2}, - }, 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}, @@ -511,11 +498,6 @@ branches: m.repositories.Set("/repo", scanner.RepoStatus{ Branches: scanner.BranchStatus{ Branch: "aaa", - 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, 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}, diff --git a/ui/update.go b/ui/update.go index f69fed9..a27dfe8 100644 --- a/ui/update.go +++ b/ui/update.go @@ -299,7 +299,7 @@ func (m *model) cycleFocus(forward bool) { } if !found { if cur == paneDiff { - // Mouse-focused Diff: Tab continues the main layout order (Branches → Diff → Log). + // Mouse-focused Diff is outside tabFocusCycle; Tab goes to Log, Shift+Tab to Branches. if forward { i = 3 // log } else { diff --git a/ui/view_test.go b/ui/view_test.go index 0b4a932..97dc160 100644 --- a/ui/view_test.go +++ b/ui/view_test.go @@ -67,11 +67,14 @@ func TestBranchTableViewFitsInnerWidth(t *testing.T) { m.repositories.Set("/repo", scanner.RepoStatus{ Branches: scanner.BranchStatus{ Branch: "main", - 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, Incoming: 1, Outgoing: 2}, - {Name: "upstream", Exists: false}, - }, + LocalBranches: []scanner.LocalBranchRef{{ + Name: "main", 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, Incoming: 1, Outgoing: 2}, + {Name: "upstream", Exists: false}, + }, + }}, }, }) m.syncViewports()