Archived
Make the superproject selectable via the header
The header stands for the root repository: it is selected on startup, shows the root's own state word, and the detail pane shows its state like a submodule's (HEAD vs. upstream, changes, unpushed, leftover submodule clones as untracked repositories). Submodules are ignored in the root's changes; their rows show them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"submarine/internal/submodule"
|
||||
)
|
||||
|
||||
// rootFixture is ownFixture with a bare origin for the superproject, main
|
||||
// pushed and tracking it. It returns the provider, the fixture root and the
|
||||
// path of the bare repository.
|
||||
func rootFixture(t *testing.T) (*Provider, string, string) {
|
||||
t.Helper()
|
||||
p, root := ownFixture(t)
|
||||
bare := filepath.Join(root, "remotes", "super.git")
|
||||
git(t, root, "init", "-q", "--bare", bare)
|
||||
git(t, p.root, "remote", "add", "origin", bare)
|
||||
git(t, p.root, "push", "-q", "-u", "origin", "main")
|
||||
return p, root, bare
|
||||
}
|
||||
|
||||
func rootStatus(t *testing.T, p *Provider) *submodule.Status {
|
||||
t.Helper()
|
||||
st, err := p.Status(context.Background(), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !st.Root {
|
||||
t.Error("Status.Root = false for the superproject")
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func issueKinds(st *submodule.Status) []submodule.IssueKind {
|
||||
var kinds []submodule.IssueKind
|
||||
for _, is := range st.Issues() {
|
||||
kinds = append(kinds, is.Kind)
|
||||
}
|
||||
return kinds
|
||||
}
|
||||
|
||||
func TestTreeRoot(t *testing.T) {
|
||||
p, root, bare := rootFixture(t)
|
||||
repo, err := p.Tree(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := repo.Root; r == nil || !r.IsRoot() || r.Name != "super" || r.URL != bare || len(r.Children) != 0 {
|
||||
t.Errorf("Root = %+v (fixture %s)", r, root)
|
||||
}
|
||||
}
|
||||
|
||||
// The fixture has dirty, conflicting and staged submodules; none of them
|
||||
// makes the superproject itself changed, their rows show them.
|
||||
func TestRootStatusIgnoresSubmodules(t *testing.T) {
|
||||
p, _, _ := rootFixture(t)
|
||||
st := rootStatus(t, p)
|
||||
if kinds := issueKinds(st); len(kinds) != 0 {
|
||||
t.Errorf("issues = %v, want none", kinds)
|
||||
}
|
||||
if st.Changes != nil || st.PinStaged || st.Presence != submodule.PresenceOK {
|
||||
t.Errorf("Changes = %+v, PinStaged = %v", st.Changes, st.PinStaged)
|
||||
}
|
||||
if st.Pin.Ref != "origin/main" || st.Pin.Source != submodule.RefFromCurrentBranch || !st.Pin.Pushed {
|
||||
t.Errorf("Pin = %+v", st.Pin)
|
||||
}
|
||||
if st.Pinned == nil || st.Pinned != st.CheckedOut && *st.Pinned != *st.CheckedOut {
|
||||
t.Errorf("Pinned = %+v, CheckedOut = %+v, want HEAD for both", st.Pinned, st.CheckedOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootStatusUnpushed(t *testing.T) {
|
||||
p, _, _ := rootFixture(t)
|
||||
git(t, p.root, "commit", "--allow-empty", "-m", "Local work")
|
||||
st := rootStatus(t, p)
|
||||
if kinds := issueKinds(st); !slices.Equal(kinds, []submodule.IssueKind{submodule.IssueUnpushed}) {
|
||||
t.Errorf("issues = %v, want unpushed", kinds)
|
||||
}
|
||||
if st.Pin.Ahead != 1 || st.Pin.Behind != 0 {
|
||||
t.Errorf("ahead/behind = %d/%d, want 1/0", st.Pin.Ahead, st.Pin.Behind)
|
||||
}
|
||||
if d := st.Issues()[0].Detail; d != "HEAD not pushed to any remote" {
|
||||
t.Errorf("detail = %q", d)
|
||||
}
|
||||
}
|
||||
|
||||
// A submodule the current branch no longer has stays on disk after a branch
|
||||
// switch (its .git is a file into .git/modules). It is an untracked
|
||||
// repository of the superproject.
|
||||
func TestRootStatusLeftoverSubmodule(t *testing.T) {
|
||||
p, _, _ := rootFixture(t)
|
||||
git(t, p.root, "switch", "-q", "-c", "nosub")
|
||||
git(t, p.root, "rm", "-q", "--cached", "src/ui")
|
||||
git(t, p.root, "commit", "-q", "-m", "Drop src/ui")
|
||||
git(t, p.root, "switch", "-q", "main")
|
||||
git(t, p.root, "switch", "-q", "nosub")
|
||||
if _, err := os.Stat(filepath.Join(p.root, "src", "ui", ".git")); err != nil {
|
||||
t.Fatalf("src/ui should be left behind: %v", err)
|
||||
}
|
||||
|
||||
st := rootStatus(t, p)
|
||||
if kinds := issueKinds(st); !slices.Equal(kinds, []submodule.IssueKind{submodule.IssueUnpushed, submodule.IssueChanged}) {
|
||||
t.Fatalf("issues = %v, want unpushed (new branch) and changed", kinds)
|
||||
}
|
||||
if d := st.Issues()[1].Detail; d != "untracked repository" {
|
||||
t.Errorf("detail = %q, want untracked repository", d)
|
||||
}
|
||||
if st.Changes == nil || len(st.Changes.Files) != 1 {
|
||||
t.Fatalf("Changes = %+v", st.Changes)
|
||||
}
|
||||
if f := st.Changes.Files[0]; f.Path != "src/ui/" || f.Kind != submodule.ChangeUntracked || !f.Repo {
|
||||
t.Errorf("file = %+v", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootStatusModified(t *testing.T) {
|
||||
p, _, _ := rootFixture(t)
|
||||
if err := os.WriteFile(filepath.Join(p.root, "README"), []byte("edited\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st := rootStatus(t, p)
|
||||
if kinds := issueKinds(st); !slices.Equal(kinds, []submodule.IssueKind{submodule.IssueChanged}) {
|
||||
t.Errorf("issues = %v, want changed", kinds)
|
||||
}
|
||||
if st.Worktree != submodule.WorktreeModified {
|
||||
t.Errorf("Worktree = %v", st.Worktree)
|
||||
}
|
||||
}
|
||||
|
||||
// The ref of the superproject is the upstream of its branch, even if another
|
||||
// remote branch is closer, so that it agrees with the header's ahead/behind.
|
||||
func TestRootStatusRefIsUpstream(t *testing.T) {
|
||||
p, root, bare := rootFixture(t)
|
||||
other := filepath.Join(root, "other")
|
||||
git(t, root, "clone", "-q", bare, other)
|
||||
git(t, other, "switch", "-q", "-c", "feat")
|
||||
git(t, other, "commit", "--allow-empty", "-m", "Feature")
|
||||
git(t, other, "push", "-q", "origin", "feat")
|
||||
git(t, other, "switch", "-q", "main")
|
||||
for range 3 {
|
||||
git(t, other, "commit", "--allow-empty", "-m", "Upstream")
|
||||
}
|
||||
git(t, other, "push", "-q", "origin", "main")
|
||||
git(t, p.root, "fetch", "-q")
|
||||
|
||||
st := rootStatus(t, p)
|
||||
if st.Pin.Ref != "origin/main" || st.Pin.Source != submodule.RefFromCurrentBranch {
|
||||
t.Errorf("ref = %q (%v), want origin/main via the current branch", st.Pin.Ref, st.Pin.Source)
|
||||
}
|
||||
if st.Pin.Ahead != 0 || st.Pin.Behind != 3 {
|
||||
t.Errorf("ahead/behind = %d/%d, want 0/3", st.Pin.Ahead, st.Pin.Behind)
|
||||
}
|
||||
if len(st.Between) != 3 {
|
||||
t.Errorf("Between = %d commits, want 3", len(st.Between))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootStatusDetached(t *testing.T) {
|
||||
p, _, _ := rootFixture(t)
|
||||
git(t, p.root, "switch", "-q", "--detach")
|
||||
st := rootStatus(t, p)
|
||||
if st.Checkout != submodule.CheckoutDetached {
|
||||
t.Errorf("Checkout = %v, want detached", st.Checkout)
|
||||
}
|
||||
if !st.Pin.Pushed {
|
||||
t.Error("detached HEAD on a pushed commit should be pushed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user