This repository has been archived on 2026-10-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
simonfrankenandClaude Sonnet 5.5 e8d366d88f Compare pins with a target branch and flag pins off it
The ref of a pin was the closest branch containing it, so a pin on a
feature branch looked like a perfect one, and the ref changed with the
branch checked out in the submodule. Split it in two:

- Pin.Target is the branch the pin should be on: `branch =` of
  .gitmodules (none if it does not exist locally), else the remote's
  default branch.
- Pin.Ref is shown: the target if the pin is on it, else the branch
  containing the pin with the fewest commits after it. A tie goes to the
  upstream of the checked-out branch, then to the first name; no branch is
  a favorite.

A pushed pin that is not on its target is `off target`, a new issue at the
new level Info (blue), counted in the header and below collapsed rows. The
spine shows the target's tip, labelled with its branch name, so the
distance from it is visible; a diverged one is the fork. The role words in
a spine label are styled separately and `you` is bold.

Add Status.TargetTip, TargetDrift and Bases.Target, Pin.Target and
Pin.Branches; drop RefSource.IsTarget. The detail pane's label column is 11
wide for the word `off target`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 17:35:16 +02:00

115 lines
4.5 KiB
Go

package submodule
import (
"errors"
"slices"
"testing"
)
func kinds(is []Issue) []IssueKind {
var out []IssueKind
for _, i := range is {
out = append(out, i.Kind)
}
return out
}
func TestIssues(t *testing.T) {
pushed := PinInfo{Pushed: true}
tests := []struct {
name string
s *Status
want []IssueKind
}{
{"loading", nil, nil},
{"clean", &Status{Pin: pushed}, nil},
{"behind, staged and detached are quiet", &Status{Pin: PinInfo{Pushed: true, Behind: 3}, PinStaged: true, Checkout: CheckoutDetached}, nil},
{"not initialized", &Status{Presence: NotInitialized}, []IssueKind{IssueMissing}},
{"dir missing", &Status{Presence: DirMissing}, []IssueKind{IssueMissing}},
{"untracked is changed", &Status{Worktree: WorktreeUntracked, Pin: pushed}, []IssueKind{IssueChanged}},
{"danger first", &Status{Checkout: CheckoutDiffers, Worktree: WorktreeModified}, []IssueKind{IssueUnpushed, IssueOffPin, IssueChanged}},
{"conflict", &Status{Worktree: WorktreeConflict, Pin: pushed}, []IssueKind{IssueConflict}},
{"pushed, but not on the target", &Status{Pin: PinInfo{Pushed: true, Ref: "origin/feat", Target: "origin/main", Source: RefFromContains}}, []IssueKind{IssueOffTarget}},
{"off target comes last", &Status{Checkout: CheckoutDiffers, Pin: PinInfo{Pushed: true, Ref: "origin/feat", Target: "origin/main"}}, []IssueKind{IssueOffPin, IssueOffTarget}},
{"unpushed is not also off target", &Status{Pin: PinInfo{Ref: "origin/main", Target: "origin/main", Ahead: 1}}, []IssueKind{IssueUnpushed}},
{"on the target, ahead of nothing else", &Status{Pin: PinInfo{Pushed: true, Ref: "origin/main", Target: "origin/main", Behind: 4}}, nil},
{"no target, no judgement", &Status{Pin: PinInfo{Pushed: true, Ref: "origin/feat", Source: RefFromContains}}, nil},
{"the superproject has no target", &Status{Root: true, Pin: PinInfo{Pushed: true, Ref: "origin/feat", Target: "origin/main"}}, nil},
}
for _, tt := range tests {
if got := kinds(tt.s.Issues()); !slices.Equal(got, tt.want) {
t.Errorf("%s: Issues() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestOffTargetText(t *testing.T) {
s := &Status{Pin: PinInfo{Target: "origin/marco-dev", Ref: "origin/fix/x"}}
for _, c := range []struct {
branches []string
want string
}{
{nil, "pin not on origin/marco-dev, on origin/fix/x"},
{[]string{"origin/main"}, "pin not on origin/marco-dev, on origin/fix/x +1"},
{[]string{"origin/main", "origin/y"}, "pin not on origin/marco-dev, on origin/fix/x +2"},
} {
s.Pin.Branches = c.branches
if got := s.offTargetText(); got != c.want {
t.Errorf("%v: %q, want %q", c.branches, got, c.want)
}
}
}
func TestHiddenIssues(t *testing.T) {
ok := &Status{Pin: PinInfo{Pushed: true}}
dirty := &Status{Worktree: WorktreeModified, Pin: PinInfo{Pushed: true}, PinStaged: true}
unpushed := &Status{}
grandchild := &Submodule{Status: unpushed}
child := &Submodule{Status: dirty, Children: []*Submodule{grandchild}}
loading := &Submodule{}
root := &Submodule{Status: ok, Children: []*Submodule{child, {Status: ok}, loading}}
if got := root.HiddenIssues(); got != (Hidden{Warn: 1, Danger: 1, Staged: 1}) {
t.Errorf("HiddenIssues() = %+v; want 1 warn, 1 danger, 1 staged", got)
}
}
func TestUntrackedRepositoryWording(t *testing.T) {
repo := FileChange{Path: "lib/x/", Kind: ChangeUntracked, Repo: true}
file := FileChange{Path: "a.txt", Kind: ChangeUntracked}
for _, c := range []struct {
files []FileChange
want string
}{
{[]FileChange{repo}, "untracked repository"},
{[]FileChange{repo, file}, "untracked files"},
{[]FileChange{file}, "untracked files"},
} {
s := &Status{Worktree: WorktreeUntracked, Changes: &Changes{Total: len(c.files), Files: c.files}}
got := s.Issues()
if len(got) == 0 || got[len(got)-1].Detail != c.want {
t.Errorf("%v: got %v, want %q", c.files, got, c.want)
}
}
}
func TestHiddenIssuesCountLoadErrors(t *testing.T) {
bad := &Submodule{Err: errors.New("boom")}
loading := &Submodule{}
parent := &Submodule{Children: []*Submodule{bad, loading}}
if got := parent.HiddenIssues(); got != (Hidden{Danger: 1}) {
t.Errorf("got %+v; want 1 danger", got)
}
}
func TestRootIssueWording(t *testing.T) {
st := &Status{Root: true}
got := st.Issues()
if len(got) != 1 || got[0].Kind != IssueUnpushed || got[0].Level != LevelDanger || got[0].Detail != "HEAD not pushed to any remote" {
t.Errorf("Issues() = %+v", got)
}
if !(&Submodule{}).IsRoot() || (&Submodule{Path: "a"}).IsRoot() {
t.Error("IsRoot should be true exactly for the empty path")
}
}