1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6 "errors"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11)
12
13func TestConfigChecks(t *testing.T) {
14 ok := []string{
15 "",
16 "[core]\n\trepositoryformatversion = 0\n\tbare = true\n",
17 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tobjectformat = sha1\n\trefstorage = files\n",
18 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tObjectFormat = SHA1 # comment\n",
19 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tnoop = true\n\tworktreeConfig = true\n",
20 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tpreciousObjects = true\n\tRelativeWorktrees = true\n",
21 "[core]\n\trepositoryformatversion = 1\n[EXTENSIONS]\n\tPRECIOUSOBJECTS = true\n\trelativeworktrees = true\n",
22 "[remote \"origin\"]\n\turl = x\n[extensions-lookalike]\n\tfoo = bar\n",
23 "# comment\n; comment\n[core]\nrepositoryformatversion=0\n",
24 "[branch \"extensions\"]\n\tobjectformat = sha256\n",
25 }
26 for _, c := range ok {
27 if err := checkConfigData([]byte(c)); err != nil {
28 t.Errorf("config %q: %v", c, err)
29 }
30 }
31 bad := map[string]string{
32 "[core]\n\trepositoryformatversion = 2\n": "repositoryformatversion",
33 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tobjectformat = sha256\n": "objectformat",
34 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefstorage = reftable\n": "refstorage",
35 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tpartialclone = origin\n": "partialclone",
36 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tcompatobjectformat = x\n": "compatobjectformat",
37 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tfutureThing = 1\n": "futurething",
38 }
39 for c, want := range bad {
40 err := checkConfigData([]byte(c))
41 if !errors.Is(err, ErrUnsupported) || !strings.Contains(err.Error(), want) {
42 t.Errorf("config %q: %v, want ErrUnsupported mentioning %q", c, err, want)
43 }
44 }
45 if err := checkConfigData([]byte("[core\n")); !errors.Is(err, ErrCorrupt) {
46 t.Errorf("broken section: %v", err)
47 }
48}
49
50func TestOpenUnsupported(t *testing.T) {
51 cases := []struct {
52 name string
53 setup func(t *testing.T, f *fx)
54 want string
55 }{
56 {"sha256", func(t *testing.T, f *fx) {
57 f.git("config", "core.repositoryformatversion", "1")
58 f.git("config", "extensions.objectformat", "sha256")
59 }, "objectformat"},
60 {"reftable", func(t *testing.T, f *fx) {
61 f.git("config", "core.repositoryformatversion", "1")
62 f.git("config", "extensions.refstorage", "reftable")
63 }, "refstorage"},
64 {"extension", func(t *testing.T, f *fx) {
65 f.git("config", "core.repositoryformatversion", "1")
66 f.git("config", "extensions.partialclone", "origin")
67 }, "partialclone"},
68 {"version", func(t *testing.T, f *fx) { f.git("config", "core.repositoryformatversion", "3") }, "formatversion"},
69 {"shallow", func(t *testing.T, f *fx) {
70 writeFile(t, f.dir+"/shallow", f.git("rev-parse", "HEAD")+"\n")
71 }, "shallow"},
72 {"grafts", func(t *testing.T, f *fx) {
73 writeFile(t, f.dir+"/info/grafts", f.git("rev-parse", "HEAD")+"\n")
74 }, "grafts"},
75 {"replace", func(t *testing.T, f *fx) {
76 old := f.git("rev-parse", "HEAD")
77 f.write("b", []byte("b"), 0o644)
78 f.commit("two")
79 f.git("replace", old, "HEAD")
80 }, "replace"},
81 {"replace-packed", func(t *testing.T, f *fx) {
82 old := f.git("rev-parse", "HEAD")
83 f.write("b", []byte("b"), 0o644)
84 f.commit("two")
85 f.git("replace", old, "HEAD")
86 f.git("pack-refs", "--all")
87 }, "replace"},
88 {"alternates", func(t *testing.T, f *fx) {
89 writeFile(t, f.dir+"/objects/info/alternates", t.TempDir()+"\n")
90 }, "alternate"},
91 {"promisor", func(t *testing.T, f *fx) {
92 f.git("repack", "-ad")
93 writeFile(t, firstPackIdx(t, f)[:len(firstPackIdx(t, f))-4]+".promisor", "")
94 }, "partial clone"},
95 {"midx-with-missing-idx", func(t *testing.T, f *fx) {
96 f.git("repack", "-ad")
97 f.git("multi-pack-index", "write")
98 if err := os.Remove(firstPackIdx(t, f)); err != nil {
99 t.Fatal(err)
100 }
101 }, "multi-pack-index"},
102 {"worktree-gitfile", nil, "gitfile"},
103 }
104 for _, c := range cases {
105 t.Run(c.name, func(t *testing.T) {
106 f := newFx(t)
107 f.write("a", []byte("a"), 0o644)
108 f.commit("one")
109 path := f.work
110 if c.setup != nil {
111 c.setup(t, f)
112 } else {
113 path = t.TempDir()
114 writeFile(t, path+"/.git", "gitdir: /elsewhere\n")
115 }
116 _, err := Open(path)
117 if !errors.Is(err, ErrUnsupported) || !strings.Contains(strings.ToLower(err.Error()), c.want) {
118 t.Fatalf("Open = %v, want ErrUnsupported mentioning %q", err, c.want)
119 }
120 })
121 }
122}
123
124func TestEmptyReplaceDirIsFine(t *testing.T) {
125 f := newFx(t)
126 f.write("a", []byte("a"), 0o644)
127 f.commit("one")
128 if err := os.MkdirAll(f.dir+"/refs/replace", 0o755); err != nil {
129 t.Fatal(err)
130 }
131 f.open()
132}
133
134func TestMidxIgnoredWhenIdxFilesPresent(t *testing.T) {
135 f := newFx(t)
136 mixedFixture(t, f)
137 f.git("repack", "-ad")
138 f.git("multi-pack-index", "write")
139 if !exists(f.dir + "/objects/pack/multi-pack-index") {
140 t.Fatal("fixture has no multi-pack-index")
141 }
142 r := f.open()
143 checkAll(t, r, f.root())
144 // Two packs and a midx covering both.
145 f.write("more.txt", []byte("more\n"), 0o644)
146 f.commit("more")
147 f.git("repack", "-d")
148 f.git("multi-pack-index", "write")
149 if err := r.Refresh(); err != nil {
150 t.Fatal(err)
151 }
152 checkAll(t, r, f.root())
153}
154
155func TestIncrementalMidxUnsupported(t *testing.T) {
156 f := newFx(t)
157 f.write("a", []byte("a"), 0o644)
158 f.commit("one")
159 f.git("repack", "-ad")
160 if out, err := f.gitErr("multi-pack-index", "write", "--incremental"); err != nil {
161 t.Skipf("this git cannot write an incremental multi-pack-index: %v %s", err, out)
162 }
163 if _, err := Open(f.work); !errors.Is(err, ErrUnsupported) {
164 t.Fatalf("Open = %v, want ErrUnsupported", err)
165 }
166}
167
168func TestRefreshSeesChanges(t *testing.T) {
169 f := newBareFx(t)
170 src := newFx(t)
171 src.write("a", []byte("a\n"), 0o644)
172 c1 := src.commit("one")
173 f.git("fetch", "-q", src.dir, "+refs/heads/main:refs/heads/main")
174 r := f.open()
175 if id, err := r.ResolveRef("refs/heads/main"); err != nil || id.String() != c1 {
176 t.Fatalf("main = %s %v", id, err)
177 }
178 // A push: new objects (a new pack), updated ref, new ref and tag.
179 src.write("b", []byte("b\n"), 0o644)
180 c2 := src.commit("two")
181 src.git("tag", "-a", "-m", "v1", "v1")
182 f.git("fetch", "-q", "--tags", src.dir, "+refs/heads/main:refs/heads/main", "+refs/heads/main:refs/heads/second")
183 if err := r.Refresh(); err != nil {
184 t.Fatal(err)
185 }
186 for _, name := range []string{"refs/heads/main", "refs/heads/second"} {
187 if id, err := r.ResolveRef(name); err != nil || id.String() != c2 {
188 t.Fatalf("%s = %s %v", name, id, err)
189 }
190 }
191 var c Commit
192 if err := r.ReadCommit(mustID(t, c2), &c); err != nil {
193 t.Fatal(err)
194 }
195 compareRefs(t, r, f.root())
196 compareObjects(t, r, f.root())
197
198 // pack-refs then a repack that deletes the old packs.
199 f.git("pack-refs", "--all")
200 f.git("repack", "-adf")
201 if err := r.Refresh(); err != nil {
202 t.Fatal(err)
203 }
204 compareRefs(t, r, f.root())
205 compareObjects(t, r, f.root())
206 if n := len(r.snap.Load().packs); n != 1 {
207 t.Fatalf("%d packs after repack -ad, want 1", n)
208 }
209}
210
211// An object that arrives in a new pack is found without Refresh.
212func TestMissingObjectRescansPacks(t *testing.T) {
213 f := newBareFx(t)
214 src := newFx(t)
215 src.write("a", []byte("a\n"), 0o644)
216 src.commit("one")
217 f.git("fetch", "-q", src.dir, "+refs/heads/main:refs/heads/main")
218 r := f.open()
219 src.write("b", []byte("b\n"), 0o644)
220 c2 := src.commit("two")
221 f.git("fetch", "-q", src.dir, "+refs/heads/main:refs/heads/main")
222 f.git("repack", "-adf")
223 var c Commit
224 if err := r.ReadCommit(mustID(t, c2), &c); err != nil {
225 t.Fatalf("new object: %v", err)
226 }
227 if _, err := r.ResolvePrefix(c2[:10]); err != nil {
228 t.Fatalf("new prefix: %v", err)
229 }
230}
231
232func TestClosedRepo(t *testing.T) {
233 f := newFx(t)
234 f.write("a", []byte("a"), 0o644)
235 c := f.commit("one")
236 r := f.open()
237 _ = r.Close()
238 _ = r.Close()
239 if _, _, err := r.ReadObject(mustID(t, c), nil); !errors.Is(err, ErrClosed) {
240 t.Fatalf("ReadObject after Close = %v", err)
241 }
242 if err := r.Refresh(); !errors.Is(err, ErrClosed) {
243 t.Fatalf("Refresh after Close = %v", err)
244 }
245}
246
247func TestRefreshDetectsNewUnsupported(t *testing.T) {
248 f := newFx(t)
249 f.write("a", []byte("a"), 0o644)
250 f.commit("one")
251 r := f.open()
252 writeFile(t, f.dir+"/shallow", f.git("rev-parse", "HEAD")+"\n")
253 if err := r.Refresh(); !errors.Is(err, ErrUnsupported) {
254 t.Fatalf("Refresh = %v, want ErrUnsupported", err)
255 }
256}
257
258func writeFile(t *testing.T, path, data string) {
259 t.Helper()
260 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
261 t.Fatal(err)
262 }
263 if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
264 t.Fatal(err)
265 }
266}