write_test.go
⎇
Raw
1package gitcmd
2
3import (
4 "context"
5 "errors"
6 "os/exec"
7 "slices"
8 "testing"
9)
10
11var testWho = Ident{Name: "Bot", Email: "bot@example.com"}
12
13// newSigningGit is newTestGit with commit signing set up.
14func newSigningGit(t *testing.T) (*Git, string) {
15 t.Helper()
16 if _, err := exec.LookPath("ssh-keygen"); err != nil {
17 t.Skip("ssh-keygen not installed")
18 }
19 g, repo := newTestGit(t)
20 if err := g.EnsureSigningSetup(); err != nil {
21 t.Fatal(err)
22 }
23 return g, repo
24}
25
26func mustShow(t *testing.T, g *Git, repo, ref, path string) string {
27 t.Helper()
28 b, err := g.Show(context.Background(), repo, ref, path)
29 if err != nil {
30 t.Fatal(err)
31 }
32 return string(b)
33}
34
35func TestEditFileAtBase(t *testing.T) {
36 g, repo := newSigningGit(t)
37 ctx := context.Background()
38 if _, err := g.EditFile(ctx, repo, "main", "", "", "other.txt", []byte("one\n"), "add", testWho); err != nil {
39 t.Fatal(err)
40 }
41 base, _ := g.ResolveRef(ctx, repo, "main")
42 _, err := g.EditFile(ctx, repo, "main", "", "other.txt", "other.txt", []byte("two\n"), "push", testWho)
43 if err != nil {
44 t.Fatal(err)
45 }
46
47 // The concurrent change did not touch hello.txt, so the edit lands on top.
48 if _, err := g.EditFile(ctx, repo, "main", base, "hello.txt", "hello.txt", []byte("edited\n"), "e", testWho); err != nil {
49 t.Fatal(err)
50 }
51 if got := mustShow(t, g, repo, "main", "other.txt"); got != "two\n" {
52 t.Fatalf("pushed change reverted: %q", got)
53 }
54
55 // other.txt changed since base, so the edit must not commit.
56 tip, _ := g.ResolveRef(ctx, repo, "main")
57 _, err = g.EditFile(ctx, repo, "main", base, "other.txt", "other.txt", []byte("mine\n"), "e", testWho)
58 var stale *StaleError
59 if !errors.As(err, &stale) || stale.Tip != tip {
60 t.Fatalf("want StaleError at %s, got %v", tip, err)
61 }
62 if now, _ := g.ResolveRef(ctx, repo, "main"); now != tip {
63 t.Fatal("stale edit moved the branch")
64 }
65 // Saving again with the new tip as base overwrites on purpose.
66 if _, err := g.EditFile(ctx, repo, "main", stale.Tip, "other.txt", "other.txt", []byte("mine\n"), "e", testWho); err != nil {
67 t.Fatal(err)
68 }
69}
70
71func TestEditFileExistenceChecks(t *testing.T) {
72 g, repo := newSigningGit(t)
73 ctx := context.Background()
74 if _, err := g.EditFile(ctx, repo, "main", "", "", "dir/a.txt", []byte("a\n"), "add", testWho); err != nil {
75 t.Fatal(err)
76 }
77 if _, err := g.EditFile(ctx, repo, "main", "", "", "hello.txt", []byte("x\n"), "c", testWho); !errors.Is(err, ErrExists) {
78 t.Fatalf("create over file: %v", err)
79 }
80 if _, err := g.EditFile(ctx, repo, "main", "", "hello.txt", "dir/a.txt", []byte("x\n"), "r", testWho); !errors.Is(err, ErrExists) {
81 t.Fatalf("rename over file: %v", err)
82 }
83 if _, err := g.EditFile(ctx, repo, "main", "", "hello.txt", "dir", []byte("x\n"), "r", testWho); !errors.Is(err, ErrExists) {
84 t.Fatalf("rename over dir: %v", err)
85 }
86 tip, _ := g.ResolveRef(ctx, repo, "main")
87 for _, p := range []string{"missing.txt", "dir"} {
88 if _, err := g.DeleteFile(ctx, repo, "main", "", p, "d", testWho); !errors.Is(err, ErrNotFound) {
89 t.Fatalf("delete %s: %v", p, err)
90 }
91 }
92 if now, _ := g.ResolveRef(ctx, repo, "main"); now != tip {
93 t.Fatal("rejected write moved the branch")
94 }
95}
96
97func TestEditFileBranchAndTagSameName(t *testing.T) {
98 g, repo := newSigningGit(t)
99 ctx := context.Background()
100 p := g.RepoPath(repo)
101 if err := g.CreateBranch(ctx, repo, "v1", "main"); err != nil {
102 t.Fatal(err)
103 }
104 if _, err := g.run(ctx, runOpts{}, "-C", p, "tag", "v1", "main"); err != nil {
105 t.Fatal(err)
106 }
107 g.InvalidateRefCache(repo)
108 if branches, _ := g.Branches(ctx, repo); !slices.Contains(branches, "heads/v1") {
109 t.Fatalf("branches = %v", branches)
110 }
111 if _, err := g.EditFile(ctx, repo, "heads/v1", "", "", "new.txt", []byte("n\n"), "add", testWho); err != nil {
112 t.Fatal(err)
113 }
114 if got := mustShow(t, g, repo, "refs/heads/v1", "new.txt"); got != "n\n" {
115 t.Fatalf("new.txt on v1 = %q", got)
116 }
117 if got := mustShow(t, g, repo, "refs/heads/v1", "hello.txt"); got != "hello archive\n" {
118 t.Fatalf("hello.txt on v1 = %q", got)
119 }
120 if _, err := g.ResolveRef(ctx, repo, "refs/heads/heads/v1"); err == nil {
121 t.Fatal("write created refs/heads/heads/v1")
122 }
123}
124
125func TestEditFileNewBranchStartsEmpty(t *testing.T) {
126 g, repo := newSigningGit(t)
127 ctx := context.Background()
128 if _, err := g.EditFile(ctx, repo, "main", "", "hello.txt", "hello.txt", []byte("x\n"), "e", testWho); err != nil {
129 t.Fatal(err)
130 }
131 if _, err := g.EditFile(ctx, repo, "orphan", "", "", "only.txt", []byte("o\n"), "add", testWho); err != nil {
132 t.Fatal(err)
133 }
134 entries, err := g.LsTree(ctx, repo, "orphan", "")
135 if err != nil {
136 t.Fatal(err)
137 }
138 if len(entries) != 1 || entries[0].Name != "only.txt" {
139 t.Fatalf("orphan tree: %+v", entries)
140 }
141}
142
143func TestCreateBranchFromAnnotatedTag(t *testing.T) {
144 g, repo := newSigningGit(t)
145 ctx := context.Background()
146 if err := g.CreateTag(ctx, repo, "rel", "main", "release", Ident{Name: "T", Email: "t@e"}); err != nil {
147 t.Fatal(err)
148 }
149 if err := g.CreateBranch(ctx, repo, "from-tag", "rel"); err != nil {
150 t.Fatal(err)
151 }
152 want, _ := g.ResolveRef(ctx, repo, "main")
153 if got, _ := g.ResolveRef(ctx, repo, "refs/heads/from-tag"); got != want {
154 t.Fatalf("branch at %s, want commit %s", got, want)
155 }
156}
157
158func TestDeleteFileAtBase(t *testing.T) {
159 g, repo := newSigningGit(t)
160 ctx := context.Background()
161 if _, err := g.EditFile(ctx, repo, "main", "", "", "a.txt", []byte("a\n"), "add", testWho); err != nil {
162 t.Fatal(err)
163 }
164 base, _ := g.ResolveRef(ctx, repo, "main")
165 if _, err := g.EditFile(ctx, repo, "main", "", "a.txt", "a.txt", []byte("pushed\n"), "push", testWho); err != nil {
166 t.Fatal(err)
167 }
168 _, err := g.DeleteFile(ctx, repo, "main", base, "a.txt", "d", testWho)
169 var stale *StaleError
170 if !errors.As(err, &stale) {
171 t.Fatalf("want StaleError, got %v", err)
172 }
173 // hello.txt did not change since base, so its delete lands on the tip.
174 if _, err := g.DeleteFile(ctx, repo, "main", base, "hello.txt", "d", testWho); err != nil {
175 t.Fatal(err)
176 }
177 if got := mustShow(t, g, repo, "main", "a.txt"); got != "pushed\n" {
178 t.Fatalf("a.txt = %q", got)
179 }
180}
181