Archived
b opens a centered popup with a search line and the local and remote branches of the selection. Enter stages the branch tip as the pin of a submodule, restores it (and everything below) to it, and puts HEAD on the branch; a remote branch gets a local tracking branch unless one exists elsewhere. Works on the superproject too. Losses are asked like restore. The backdrop behind the popup is dimmed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
182 lines
5.0 KiB
Go
182 lines
5.0 KiB
Go
package git
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"submarine/internal/submodule"
|
|
)
|
|
|
|
func findBranch(t *testing.T, bs []submodule.Branch, name string) submodule.Branch {
|
|
t.Helper()
|
|
for _, b := range bs {
|
|
if b.Name == name {
|
|
return b
|
|
}
|
|
}
|
|
t.Fatalf("no branch %q in %v", name, bs)
|
|
return submodule.Branch{}
|
|
}
|
|
|
|
func TestBranchesListsLocalAndRemote(t *testing.T) {
|
|
p, root := ownFixture(t)
|
|
_ = root
|
|
bs, err := p.Branches(context.Background(), "libs/feature")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var names []string
|
|
seenRemote := false
|
|
for _, b := range bs {
|
|
names = append(names, b.Name)
|
|
if b.Remote {
|
|
seenRemote = true
|
|
} else if seenRemote {
|
|
t.Errorf("local branch %s after a remote one: %v", b.Name, names)
|
|
}
|
|
if b.Name == "origin/HEAD" {
|
|
t.Error("origin/HEAD must not be listed")
|
|
}
|
|
}
|
|
rb := findBranch(t, bs, "origin/feat/search")
|
|
if !rb.Remote || rb.Local != "feat/search" {
|
|
t.Errorf("origin/feat/search = %+v", rb)
|
|
}
|
|
findBranch(t, bs, "origin/main")
|
|
}
|
|
|
|
func TestSwitchSubmoduleStagesTipAndTracksRemoteBranch(t *testing.T) {
|
|
p, root := ownFixture(t)
|
|
super := filepath.Join(root, "super")
|
|
sub := filepath.Join(super, "libs", "feature")
|
|
ctx := context.Background()
|
|
|
|
bs, err := p.Branches(ctx, "libs/feature")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := findBranch(t, bs, "origin/feat/search")
|
|
got, err := p.Switch(ctx, "libs/feature", b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "feat/search" {
|
|
t.Errorf("Switch returned %q, want feat/search", got)
|
|
}
|
|
if head := out(t, sub, "branch", "--show-current"); head != "feat/search" {
|
|
t.Errorf("branch = %q", head)
|
|
}
|
|
if up := out(t, sub, "rev-parse", "--abbrev-ref", "feat/search@{upstream}"); up != "origin/feat/search" {
|
|
t.Errorf("upstream = %q", up)
|
|
}
|
|
if head := out(t, sub, "rev-parse", "HEAD"); head != b.SHA {
|
|
t.Errorf("HEAD = %s, want %s", head, b.SHA)
|
|
}
|
|
if staged := out(t, super, "ls-files", "-s", "--", "libs/feature"); !strings.Contains(staged, b.SHA) {
|
|
t.Errorf("index = %q, want the tip %s staged", staged, b.SHA)
|
|
}
|
|
assertExact(t, sub)
|
|
}
|
|
|
|
func TestSwitchNeverMovesAnExistingLocalBranch(t *testing.T) {
|
|
p, root := ownFixture(t)
|
|
sub := filepath.Join(root, "super", "libs", "feature")
|
|
ctx := context.Background()
|
|
|
|
git(t, sub, "branch", "feat/search", "origin/main") // same name, elsewhere
|
|
before := out(t, sub, "rev-parse", "feat/search")
|
|
bs, err := p.Branches(ctx, "libs/feature")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := findBranch(t, bs, "origin/feat/search")
|
|
got, err := p.Switch(ctx, "libs/feature", b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "" {
|
|
t.Errorf("Switch returned %q, want detached", got)
|
|
}
|
|
if head := out(t, sub, "branch", "--show-current"); head != "" {
|
|
t.Errorf("HEAD on %q, want detached", head)
|
|
}
|
|
if after := out(t, sub, "rev-parse", "feat/search"); after != before {
|
|
t.Errorf("branch moved: %s -> %s", before, after)
|
|
}
|
|
if head := out(t, sub, "rev-parse", "HEAD"); head != b.SHA {
|
|
t.Errorf("HEAD = %s, want %s", head, b.SHA)
|
|
}
|
|
}
|
|
|
|
func TestSwitchAbortsWhenTheBranchMoved(t *testing.T) {
|
|
p, root := ownFixture(t)
|
|
sub := filepath.Join(root, "super", "libs", "feature")
|
|
ctx := context.Background()
|
|
|
|
bs, err := p.Branches(ctx, "libs/feature")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := findBranch(t, bs, "origin/main")
|
|
b.SHA = out(t, sub, "rev-parse", "origin/feat/search") // what the list showed is stale
|
|
if _, err := p.Switch(ctx, "libs/feature", b); err == nil || !strings.Contains(err.Error(), "moved") {
|
|
t.Errorf("err = %v, want a moved error", err)
|
|
}
|
|
}
|
|
|
|
func TestSwitchSuperprojectRestoresSubmodulesToItsPins(t *testing.T) {
|
|
p, root := ownFixture(t)
|
|
super := filepath.Join(root, "super")
|
|
ctx := context.Background()
|
|
|
|
git(t, super, "branch", "older", "HEAD~1")
|
|
bs, err := p.Branches(ctx, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := findBranch(t, bs, "older")
|
|
got, err := p.Switch(ctx, "", b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "older" || out(t, super, "branch", "--show-current") != "older" {
|
|
t.Errorf("Switch = %q, branch = %q", got, out(t, super, "branch", "--show-current"))
|
|
}
|
|
assertExact(t, super)
|
|
}
|
|
|
|
func TestSwitchLossCountsChangedFiles(t *testing.T) {
|
|
p, root := ownFixture(t)
|
|
sub := filepath.Join(root, "super", "libs", "feature")
|
|
ctx := context.Background()
|
|
|
|
bs, err := p.Branches(ctx, "libs/feature")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := findBranch(t, bs, "origin/feat/search")
|
|
loss, err := p.SwitchLoss(ctx, "libs/feature", b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !loss.Empty() {
|
|
t.Errorf("clean checkout: loss = %+v", loss)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(sub, "new.txt"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if loss, err = p.SwitchLoss(ctx, "libs/feature", b); err != nil || loss.Files != 1 {
|
|
t.Errorf("loss = %+v, %v; want 1 file", loss, err)
|
|
}
|
|
}
|
|
|
|
func TestBranchesOfAMissingSubmoduleSaysSo(t *testing.T) {
|
|
p, _ := ownFixture(t)
|
|
if _, err := p.Branches(context.Background(), "libs/missing"); err == nil || !strings.Contains(err.Error(), "not initialized") {
|
|
t.Errorf("err = %v", err)
|
|
}
|
|
}
|