v0.1.1 de06cbd

open_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 2

↑↓ moveenter openesc close