Archived
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>
This commit is contained in:
@@ -90,26 +90,29 @@ func TestStatus(t *testing.T) {
|
||||
path string
|
||||
issues []submodule.IssueKind
|
||||
ref string
|
||||
target string // the target if the pin is not on it; else the ref is the target
|
||||
source submodule.RefSource
|
||||
ahead int
|
||||
behind int
|
||||
staged bool
|
||||
checkout submodule.Checkout
|
||||
drift submodule.Distance // checkout compared with the pin
|
||||
branches []string // other remote branches containing the pin
|
||||
toTarget submodule.Distance // target tip compared with the pin; zero if the pin is on it
|
||||
}{
|
||||
{path: "src/core", issues: []submodule.IssueKind{submodule.IssueOffPin}, ref: "origin/main", source: submodule.RefFromDefaultBranch, checkout: submodule.CheckoutDiffers, drift: submodule.Distance{Behind: 1}},
|
||||
{path: "src/ui", ref: "origin/main", source: submodule.RefFromCurrentBranch, behind: 2},
|
||||
{path: "src/net", issues: []submodule.IssueKind{submodule.IssueUnpushed}, ref: "origin/main", source: submodule.RefFromCurrentBranch, ahead: 1},
|
||||
{path: "src/ui", ref: "origin/main", source: submodule.RefFromDefaultBranch, behind: 2},
|
||||
{path: "src/net", issues: []submodule.IssueKind{submodule.IssueUnpushed}, ref: "origin/main", source: submodule.RefFromDefaultBranch, ahead: 1},
|
||||
{path: "libs/store", ref: "origin/develop", source: submodule.RefFromGitmodules},
|
||||
{path: "libs/log", ref: "origin/main", source: submodule.RefFromCurrentBranch},
|
||||
{path: "libs/log", ref: "origin/main", source: submodule.RefFromDefaultBranch, branches: []string{"origin/archive"}},
|
||||
{path: "libs/log/modules/nested", issues: []submodule.IssueKind{submodule.IssueChanged}, ref: "origin/main", source: submodule.RefFromDefaultBranch, checkout: submodule.CheckoutDetached},
|
||||
{path: "libs/feature", ref: "origin/feat/search", source: submodule.RefFromContains, behind: 1, checkout: submodule.CheckoutDetached},
|
||||
{path: "libs/topic", ref: "origin/feat/search", source: submodule.RefFromContains, checkout: submodule.CheckoutDetached},
|
||||
{path: "libs/conflict", issues: []submodule.IssueKind{submodule.IssueConflict}, ref: "origin/main", source: submodule.RefFromCurrentBranch, staged: true},
|
||||
{path: "libs/staged", ref: "origin/main", source: submodule.RefFromCurrentBranch, staged: true},
|
||||
{path: "libs/feature", issues: []submodule.IssueKind{submodule.IssueOffTarget}, ref: "origin/feat/search", source: submodule.RefFromContains, target: "origin/main", behind: 1, checkout: submodule.CheckoutDetached, toTarget: submodule.Distance{Behind: 1}},
|
||||
{path: "libs/topic", issues: []submodule.IssueKind{submodule.IssueOffTarget}, ref: "origin/feat/search", source: submodule.RefFromContains, target: "origin/main", checkout: submodule.CheckoutDetached, toTarget: submodule.Distance{Behind: 2}},
|
||||
{path: "libs/conflict", issues: []submodule.IssueKind{submodule.IssueConflict}, ref: "origin/main", source: submodule.RefFromDefaultBranch, staged: true},
|
||||
{path: "libs/staged", ref: "origin/main", source: submodule.RefFromDefaultBranch, staged: true},
|
||||
{path: "libs/missing", issues: []submodule.IssueKind{submodule.IssueMissing}},
|
||||
{path: "vendor", issues: []submodule.IssueKind{submodule.IssueChanged}, ref: "origin/main", source: submodule.RefFromCurrentBranch, behind: 2},
|
||||
{path: "libs/work", issues: []submodule.IssueKind{submodule.IssueChanged}, ref: "origin/main", source: submodule.RefFromCurrentBranch},
|
||||
{path: "vendor", issues: []submodule.IssueKind{submodule.IssueChanged}, ref: "origin/main", source: submodule.RefFromDefaultBranch, behind: 2},
|
||||
{path: "libs/work", issues: []submodule.IssueKind{submodule.IssueChanged}, ref: "origin/main", source: submodule.RefFromDefaultBranch},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
st, err := p.Status(context.Background(), tt.path)
|
||||
@@ -130,6 +133,13 @@ func TestStatus(t *testing.T) {
|
||||
if st.Pin.Ref != tt.ref || st.Pin.Source != tt.source {
|
||||
t.Errorf("%s: ref %q (%v), want %q (%v)", tt.path, st.Pin.Ref, st.Pin.Source, tt.ref, tt.source)
|
||||
}
|
||||
want := tt.target
|
||||
if want == "" && (tt.source == submodule.RefFromGitmodules || tt.source == submodule.RefFromDefaultBranch) {
|
||||
want = tt.ref
|
||||
}
|
||||
if st.Pin.Target != want {
|
||||
t.Errorf("%s: target %q, want %q", tt.path, st.Pin.Target, want)
|
||||
}
|
||||
if st.Pin.Ahead != tt.ahead || st.Pin.Behind != tt.behind {
|
||||
t.Errorf("%s: ↑%d↓%d, want ↑%d↓%d", tt.path, st.Pin.Ahead, st.Pin.Behind, tt.ahead, tt.behind)
|
||||
}
|
||||
@@ -142,6 +152,12 @@ func TestStatus(t *testing.T) {
|
||||
if tt.path == "libs/staged" && (st.CommittedDrift.Ahead != 0 || st.CommittedDrift.Behind == 0) {
|
||||
t.Errorf("%s: CommittedDrift %+v, want the committed pin behind the staged one", tt.path, st.CommittedDrift)
|
||||
}
|
||||
if !slices.Equal(st.Pin.Branches, tt.branches) {
|
||||
t.Errorf("%s: other branches %v, want %v", tt.path, st.Pin.Branches, tt.branches)
|
||||
}
|
||||
if st.TargetDrift != tt.toTarget || (st.TargetTip != nil) != (tt.toTarget != submodule.Distance{}) {
|
||||
t.Errorf("%s: TargetDrift %+v with tip %+v, want %+v", tt.path, st.TargetDrift, st.TargetTip, tt.toTarget)
|
||||
}
|
||||
if st.Drift != tt.drift {
|
||||
t.Errorf("%s: drift %+v, want %+v", tt.path, st.Drift, tt.drift)
|
||||
}
|
||||
@@ -334,6 +350,22 @@ func TestBasesOfDivergedCommits(t *testing.T) {
|
||||
t.Errorf("src/core: Bases.Checkout = %+v", b)
|
||||
}
|
||||
|
||||
// libs/feature: its pin is on a feature branch, and main moved on
|
||||
pushUpstream(t, root, "feature")
|
||||
if err := p.Fetch(ctx, "libs/feature"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err = p.Status(ctx, "libs/feature")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.TargetDrift != (submodule.Distance{Ahead: 1, Behind: 1}) || st.Bases.Target == nil {
|
||||
t.Errorf("libs/feature: TargetDrift %+v, Bases.Target %+v, want a diverged target with a base", st.TargetDrift, st.Bases.Target)
|
||||
}
|
||||
if sp := st.Spine(); len(sp) == 0 || !sp[0].Fork || sp[0].Roles[0] != submodule.RoleTarget {
|
||||
t.Errorf("libs/feature: spine = %+v, want a fork headed by the target", sp)
|
||||
}
|
||||
|
||||
// in sync and linear: no bases
|
||||
if st, err = p.Status(ctx, "src/ui"); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -341,3 +373,59 @@ func TestBasesOfDivergedCommits(t *testing.T) {
|
||||
t.Errorf("src/ui: Bases = %+v, want none", st.Bases)
|
||||
}
|
||||
}
|
||||
|
||||
// A `branch =` that does not exist locally is not replaced by the default
|
||||
// branch: judging the pin against the wrong branch would be worse than not
|
||||
// judging it.
|
||||
func TestDeclaredBranchMissing(t *testing.T) {
|
||||
p, root := ownFixture(t)
|
||||
git(t, filepath.Join(root, "super"), "config", "-f", ".gitmodules", "submodule.libs/store.branch", "gone")
|
||||
if _, err := p.Tree(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := p.Status(context.Background(), "libs/store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.Pin.Target != "" {
|
||||
t.Errorf("target %q, want none", st.Pin.Target)
|
||||
}
|
||||
for _, is := range st.Issues() {
|
||||
if is.Kind == submodule.IssueOffTarget {
|
||||
t.Errorf("issue %+v without a target", is)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Two branches contain the pin equally close: the branch the submodule is on
|
||||
// wins, else the first name; the default branch is no favorite.
|
||||
func TestContainingBranchTie(t *testing.T) {
|
||||
p, root := ownFixture(t)
|
||||
super := filepath.Join(root, "super")
|
||||
// the target is a branch that does not exist, so none applies
|
||||
git(t, super, "config", "-f", ".gitmodules", "submodule.libs/log.branch", "gone")
|
||||
if _, err := p.Tree(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ref := func() (string, submodule.RefSource, []string) {
|
||||
st, err := p.Status(context.Background(), "libs/log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return st.Pin.Ref, st.Pin.Source, st.Pin.Branches
|
||||
}
|
||||
|
||||
// on branch main (upstream origin/main) in the fixture
|
||||
if r, src, _ := ref(); r != "origin/main" || src != submodule.RefFromContains {
|
||||
t.Errorf("on a branch: ref %q (%v), want origin/main", r, src)
|
||||
}
|
||||
|
||||
git(t, filepath.Join(super, "libs", "log"), "checkout", "-q", "--detach")
|
||||
r, src, others := ref()
|
||||
if r != "origin/archive" || src != submodule.RefFromContains {
|
||||
t.Errorf("detached: ref %q (%v), want origin/archive", r, src)
|
||||
}
|
||||
if !slices.Equal(others, []string{"origin/main"}) {
|
||||
t.Errorf("detached: other branches %v, want [origin/main]", others)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user