package git_test import ( "context" "os" "os/exec" "path/filepath" "testing" "superwork-tui/internal/git" ) // initBareAndClone creates a bare origin repo and a local clone with one commit. func initBareAndClone(t *testing.T) (origin, clone string) { t.Helper() origin = t.TempDir() clone = t.TempDir() run := func(dir string, args ...string) { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } run(origin, "init", "--bare") run(clone, "init") run(clone, "config", "user.email", "test@test.com") run(clone, "config", "user.name", "test") run(clone, "remote", "add", "origin", origin) f := filepath.Join(clone, "README") if err := os.WriteFile(f, []byte("init"), 0o644); err != nil { t.Fatal(err) } run(clone, "add", ".") run(clone, "commit", "-m", "init") run(clone, "push", "origin", "HEAD:refs/heads/main") run(clone, "push", "origin", "HEAD:refs/heads/build") return origin, clone } func TestCheckBranchSync_SameBranch_Unavailable(t *testing.T) { _, clone := initBareAndClone(t) status, err := git.CheckBranchSync(context.Background(), git.BranchSyncOpts{ WorkspaceRoot: clone, DevBranch: "main", AutoBuildBranch: "main", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if !status.Unavailable { t.Error("same branch should be Unavailable") } } func TestCheckBranchSync_NotGitRepo_Unavailable(t *testing.T) { dir := t.TempDir() status, err := git.CheckBranchSync(context.Background(), git.BranchSyncOpts{ WorkspaceRoot: dir, DevBranch: "main", AutoBuildBranch: "build", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if !status.Unavailable { t.Error("non-git-repo should be Unavailable") } } func TestCheckBranchSync_ZeroBehind(t *testing.T) { _, clone := initBareAndClone(t) status, err := git.CheckBranchSync(context.Background(), git.BranchSyncOpts{ WorkspaceRoot: clone, DevBranch: "main", AutoBuildBranch: "build", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if status.Unavailable { t.Errorf("should be available, reason: %s", status.Reason) } if status.Behind != 0 { t.Errorf("Behind = %d, want 0 (branches are in sync)", status.Behind) } } func TestCheckBranchSync_NonZeroBehind(t *testing.T) { _, clone := initBareAndClone(t) run := func(args ...string) { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = clone if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } // Advance main by two commits; build stays behind. for i := 0; i < 2; i++ { f := filepath.Join(clone, "f") if err := os.WriteFile(f, []byte{byte(i)}, 0o644); err != nil { t.Fatal(err) } run("add", ".") run("commit", "-m", "advance") } run("push", "origin", "HEAD:refs/heads/main") status, err := git.CheckBranchSync(context.Background(), git.BranchSyncOpts{ WorkspaceRoot: clone, DevBranch: "main", AutoBuildBranch: "build", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if status.Unavailable { t.Fatalf("should be available, reason: %s", status.Reason) } if status.Behind != 2 { t.Errorf("Behind = %d, want 2", status.Behind) } } func TestRunBranchSync_AdvancesAutoBuild(t *testing.T) { origin, clone := initBareAndClone(t) run := func(args ...string) { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = clone if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } f := filepath.Join(clone, "f") if err := os.WriteFile(f, []byte("x"), 0o644); err != nil { t.Fatal(err) } run("add", ".") run("commit", "-m", "advance") run("push", "origin", "HEAD:refs/heads/main") revOf := func(ref string) string { t.Helper() cmd := exec.Command("git", "-C", origin, "rev-parse", ref) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("rev-parse %s: %v\n%s", ref, err, out) } return string(out) } if revOf("main") == revOf("build") { t.Fatal("precondition: build should be behind main") } if err := git.RunBranchSync(context.Background(), git.BranchSyncOpts{ WorkspaceRoot: clone, DevBranch: "main", AutoBuildBranch: "build", }); err != nil { t.Fatalf("RunBranchSync: %v", err) } if revOf("main") != revOf("build") { t.Errorf("build should have advanced to main; main=%q build=%q", revOf("main"), revOf("build")) } } func TestDeleteLocalBranch_NonExistent_NoOp(t *testing.T) { root := initRepo(t) err := git.DeleteLocalBranch(context.Background(), root, "nonexistent-branch") if err != nil { t.Errorf("DeleteLocalBranch on nonexistent branch should be no-op, got: %v", err) } } func TestDeleteLocalBranch_Exists_Deletes(t *testing.T) { root := initRepo(t) run := func(args ...string) { cmd := exec.Command("git", args...) cmd.Dir = root if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } run("branch", "temp-branch") err := git.DeleteLocalBranch(context.Background(), root, "temp-branch") if err != nil { t.Errorf("DeleteLocalBranch: %v", err) } // Verify branch is gone. cmd := exec.Command("git", "-C", root, "show-ref", "--verify", "refs/heads/temp-branch") if cmd.Run() == nil { t.Error("branch should have been deleted") } }