package gitcmd import ( "context" "errors" "os/exec" "slices" "testing" ) var testWho = Ident{Name: "Bot", Email: "bot@example.com"} // newSigningGit is newTestGit with commit signing set up. func newSigningGit(t *testing.T) (*Git, string) { t.Helper() if _, err := exec.LookPath("ssh-keygen"); err != nil { t.Skip("ssh-keygen not installed") } g, repo := newTestGit(t) if err := g.EnsureSigningSetup(); err != nil { t.Fatal(err) } return g, repo } func mustShow(t *testing.T, g *Git, repo, ref, path string) string { t.Helper() b, err := g.Show(context.Background(), repo, ref, path) if err != nil { t.Fatal(err) } return string(b) } func TestEditFileAtBase(t *testing.T) { g, repo := newSigningGit(t) ctx := context.Background() if _, err := g.EditFile(ctx, repo, "main", "", "", "other.txt", []byte("one\n"), "add", testWho); err != nil { t.Fatal(err) } base, _ := g.ResolveRef(ctx, repo, "main") _, err := g.EditFile(ctx, repo, "main", "", "other.txt", "other.txt", []byte("two\n"), "push", testWho) if err != nil { t.Fatal(err) } // The concurrent change did not touch hello.txt, so the edit lands on top. if _, err := g.EditFile(ctx, repo, "main", base, "hello.txt", "hello.txt", []byte("edited\n"), "e", testWho); err != nil { t.Fatal(err) } if got := mustShow(t, g, repo, "main", "other.txt"); got != "two\n" { t.Fatalf("pushed change reverted: %q", got) } // other.txt changed since base, so the edit must not commit. tip, _ := g.ResolveRef(ctx, repo, "main") _, err = g.EditFile(ctx, repo, "main", base, "other.txt", "other.txt", []byte("mine\n"), "e", testWho) var stale *StaleError if !errors.As(err, &stale) || stale.Tip != tip { t.Fatalf("want StaleError at %s, got %v", tip, err) } if now, _ := g.ResolveRef(ctx, repo, "main"); now != tip { t.Fatal("stale edit moved the branch") } // Saving again with the new tip as base overwrites on purpose. if _, err := g.EditFile(ctx, repo, "main", stale.Tip, "other.txt", "other.txt", []byte("mine\n"), "e", testWho); err != nil { t.Fatal(err) } } func TestEditFileExistenceChecks(t *testing.T) { g, repo := newSigningGit(t) ctx := context.Background() if _, err := g.EditFile(ctx, repo, "main", "", "", "dir/a.txt", []byte("a\n"), "add", testWho); err != nil { t.Fatal(err) } if _, err := g.EditFile(ctx, repo, "main", "", "", "hello.txt", []byte("x\n"), "c", testWho); !errors.Is(err, ErrExists) { t.Fatalf("create over file: %v", err) } if _, err := g.EditFile(ctx, repo, "main", "", "hello.txt", "dir/a.txt", []byte("x\n"), "r", testWho); !errors.Is(err, ErrExists) { t.Fatalf("rename over file: %v", err) } if _, err := g.EditFile(ctx, repo, "main", "", "hello.txt", "dir", []byte("x\n"), "r", testWho); !errors.Is(err, ErrExists) { t.Fatalf("rename over dir: %v", err) } tip, _ := g.ResolveRef(ctx, repo, "main") for _, p := range []string{"missing.txt", "dir"} { if _, err := g.DeleteFile(ctx, repo, "main", "", p, "d", testWho); !errors.Is(err, ErrNotFound) { t.Fatalf("delete %s: %v", p, err) } } if now, _ := g.ResolveRef(ctx, repo, "main"); now != tip { t.Fatal("rejected write moved the branch") } } func TestEditFileBranchAndTagSameName(t *testing.T) { g, repo := newSigningGit(t) ctx := context.Background() p := g.RepoPath(repo) if err := g.CreateBranch(ctx, repo, "v1", "main"); err != nil { t.Fatal(err) } if _, err := g.run(ctx, runOpts{}, "-C", p, "tag", "v1", "main"); err != nil { t.Fatal(err) } g.InvalidateRefCache(repo) if branches, _ := g.Branches(ctx, repo); !slices.Contains(branches, "heads/v1") { t.Fatalf("branches = %v", branches) } if _, err := g.EditFile(ctx, repo, "heads/v1", "", "", "new.txt", []byte("n\n"), "add", testWho); err != nil { t.Fatal(err) } if got := mustShow(t, g, repo, "refs/heads/v1", "new.txt"); got != "n\n" { t.Fatalf("new.txt on v1 = %q", got) } if got := mustShow(t, g, repo, "refs/heads/v1", "hello.txt"); got != "hello archive\n" { t.Fatalf("hello.txt on v1 = %q", got) } if _, err := g.ResolveRef(ctx, repo, "refs/heads/heads/v1"); err == nil { t.Fatal("write created refs/heads/heads/v1") } } func TestEditFileNewBranchStartsEmpty(t *testing.T) { g, repo := newSigningGit(t) ctx := context.Background() if _, err := g.EditFile(ctx, repo, "main", "", "hello.txt", "hello.txt", []byte("x\n"), "e", testWho); err != nil { t.Fatal(err) } if _, err := g.EditFile(ctx, repo, "orphan", "", "", "only.txt", []byte("o\n"), "add", testWho); err != nil { t.Fatal(err) } entries, err := g.LsTree(ctx, repo, "orphan", "") if err != nil { t.Fatal(err) } if len(entries) != 1 || entries[0].Name != "only.txt" { t.Fatalf("orphan tree: %+v", entries) } } func TestCreateBranchFromAnnotatedTag(t *testing.T) { g, repo := newSigningGit(t) ctx := context.Background() if err := g.CreateTag(ctx, repo, "rel", "main", "release", Ident{Name: "T", Email: "t@e"}); err != nil { t.Fatal(err) } if err := g.CreateBranch(ctx, repo, "from-tag", "rel"); err != nil { t.Fatal(err) } want, _ := g.ResolveRef(ctx, repo, "main") if got, _ := g.ResolveRef(ctx, repo, "refs/heads/from-tag"); got != want { t.Fatalf("branch at %s, want commit %s", got, want) } } func TestDeleteFileAtBase(t *testing.T) { g, repo := newSigningGit(t) ctx := context.Background() if _, err := g.EditFile(ctx, repo, "main", "", "", "a.txt", []byte("a\n"), "add", testWho); err != nil { t.Fatal(err) } base, _ := g.ResolveRef(ctx, repo, "main") if _, err := g.EditFile(ctx, repo, "main", "", "a.txt", "a.txt", []byte("pushed\n"), "push", testWho); err != nil { t.Fatal(err) } _, err := g.DeleteFile(ctx, repo, "main", base, "a.txt", "d", testWho) var stale *StaleError if !errors.As(err, &stale) { t.Fatalf("want StaleError, got %v", err) } // hello.txt did not change since base, so its delete lands on the tip. if _, err := g.DeleteFile(ctx, repo, "main", base, "hello.txt", "d", testWho); err != nil { t.Fatal(err) } if got := mustShow(t, g, repo, "main", "a.txt"); got != "pushed\n" { t.Fatalf("a.txt = %q", got) } }