write_test.go
| 1 | package gitcmd |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os/exec" |
| 7 | "slices" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | var testWho = Ident{Name: "Bot", Email: "bot@example.com"} |
| 12 | |
| 13 | // newSigningGit is newTestGit with commit signing set up. |
| 14 | func 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 | |
| 26 | func 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 | |
| 35 | func 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 | |
| 71 | func 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 | |
| 97 | func 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 | |
| 125 | func 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 | |
| 143 | func 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 | |
| 158 | func 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 |