1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6 "bytes"
7 "errors"
8 "fmt"
9 "io"
10 "sync"
11 "sync/atomic"
12 "testing"
13 "time"
14)
15
16// TestRefreshDuringRepack runs readers while git repacks (deleting the packs
17// they read from), prunes and packs refs, and the Repo is refreshed. No read
18// may fail, and every read must return the right content.
19func TestRefreshDuringRepack(t *testing.T) {
20 f := newFx(t)
21 var ids []ObjectID
22 var blobs [][]byte
23 for i := 0; i < 40; i++ {
24 data := append(textLines(500, i%3), []byte(fmt.Sprintf("rev %d\n", i))...)
25 f.write("f.txt", data, 0o644)
26 f.write(fmt.Sprintf("d/%d.txt", i), []byte(fmt.Sprint(i)), 0o644)
27 f.commit(fmt.Sprintf("c%d", i))
28 ids = append(ids, mustID(t, f.git("rev-parse", "HEAD:f.txt")))
29 blobs = append(blobs, data)
30 }
31 f.git("repack", "-adf", "--depth=50", "--window=50")
32 r := f.open(WithDeltaCacheSize(1 << 20))
33
34 stop := make(chan struct{})
35 var wg sync.WaitGroup
36 var reads atomic.Int64
37 var failures atomic.Int64
38 fail := func(format string, a ...any) {
39 failures.Add(1)
40 t.Errorf(format, a...)
41 }
42 for w := 0; w < 6; w++ {
43 wg.Add(1)
44 go func(w int) {
45 defer wg.Done()
46 var buf []byte
47 for i := w; ; i++ {
48 select {
49 case <-stop:
50 return
51 default:
52 }
53 k := i % len(ids)
54 switch i % 4 {
55 case 0:
56 typ, got, err := r.ReadObject(ids[k], buf)
57 if err != nil || typ != TypeBlob || !bytes.Equal(got, blobs[k]) {
58 fail("ReadObject: %v", err)
59 return
60 }
61 buf = got
62 case 1:
63 size, rc, err := r.OpenBlob(ids[k])
64 if err != nil {
65 fail("OpenBlob: %v", err)
66 return
67 }
68 // Read slowly so the repack happens meanwhile.
69 got, err := io.ReadAll(io.LimitReader(rc, 1<<20))
70 _ = rc.Close()
71 if err != nil || size != int64(len(blobs[k])) || !bytes.Equal(got, blobs[k]) {
72 fail("blob read: %v", err)
73 return
74 }
75 case 2:
76 if _, n, err := r.ObjectHeader(ids[k]); err != nil || n != int64(len(blobs[k])) {
77 fail("ObjectHeader: %v", err)
78 return
79 }
80 case 3:
81 if _, err := r.ResolvePrefix(ids[k].String()[:12]); err != nil && !errors.Is(err, ErrAmbiguous) {
82 fail("ResolvePrefix: %v", err)
83 return
84 }
85 for ref, err := range r.Refs("refs/") {
86 if err != nil {
87 fail("Refs: %v", err)
88 return
89 }
90 if ref.Name == "refs/heads/main" && ref.ID.IsZero() {
91 fail("main is zero")
92 return
93 }
94 }
95 }
96 reads.Add(1)
97 }
98 }(w)
99 }
100 // Refresher.
101 wg.Add(1)
102 go func() {
103 defer wg.Done()
104 for {
105 select {
106 case <-stop:
107 return
108 default:
109 }
110 if err := r.Refresh(); err != nil {
111 fail("Refresh: %v", err)
112 return
113 }
114 time.Sleep(time.Millisecond)
115 }
116 }()
117
118 // The writer: repack -ad (deletes the packs), new commits, pack-refs, prune.
119 for round := 0; round < 12 && failures.Load() == 0; round++ {
120 f.write("w.txt", []byte(fmt.Sprint(round)), 0o644)
121 f.commit(fmt.Sprintf("w%d", round))
122 f.git("tag", fmt.Sprintf("t%d", round))
123 switch round % 3 {
124 case 0:
125 f.git("repack", "-ad")
126 case 1:
127 f.git("repack", "-adf", "--depth=20", "--window=20")
128 f.git("pack-refs", "--all")
129 case 2:
130 f.git("gc", "--prune=now", "-q")
131 }
132 }
133 close(stop)
134 wg.Wait()
135 if failures.Load() != 0 {
136 t.FailNow()
137 }
138 if err := r.Refresh(); err != nil {
139 t.Fatal(err)
140 }
141 t.Logf("%d reads during 12 repack rounds", reads.Load())
142 compareRefs(t, r, f.root())
143 compareObjects(t, r, f.root())
144 // Packs that were replaced are unmapped once no reader needs them.
145 if n := len(r.snap.Load().packs); n != 1 {
146 t.Errorf("%d packs left in the snapshot", n)
147 }
148}
149
150// A reader blocked on an old pack keeps it mapped until it is closed.
151func TestOldPackStaysMappedWhileInUse(t *testing.T) {
152 f := newFx(t)
153 big := textLines(100000, 1)
154 f.write("big", big, 0o644)
155 f.commit("one")
156 f.git("repack", "-adf")
157 r := f.open()
158 id := mustID(t, f.git("rev-parse", "HEAD:big"))
159 _, rc, err := r.OpenBlob(id)
160 if err != nil {
161 t.Fatal(err)
162 }
163 old := r.snap.Load().packs[0]
164 buf := make([]byte, 1000)
165 if _, err := io.ReadFull(rc, buf); err != nil {
166 t.Fatal(err)
167 }
168 f.write("more", []byte("more"), 0o644)
169 f.commit("two")
170 f.git("repack", "-adf") // deletes the pack that rc reads from
171 if err := r.Refresh(); err != nil {
172 t.Fatal(err)
173 }
174 if r.snap.Load().packs[0] == old {
175 t.Fatal("the pack list did not change")
176 }
177 if old.data == nil || old.refs.Load() != 1 {
178 t.Fatalf("old pack released while in use (refs=%d)", old.refs.Load())
179 }
180 rest, err := io.ReadAll(rc)
181 if err != nil || !bytes.Equal(append(buf, rest...), big) {
182 t.Fatalf("read after repack: %v", err)
183 }
184 _ = rc.Close()
185 if old.data != nil {
186 t.Fatal("old pack still mapped after the last reader closed")
187 }
188}
189
190// A ref-delta cycle must not loop forever.
191func TestRefDeltaCycle(t *testing.T) {
192 // Two ref-deltas that are each other's base.
193 idA, idB := ObjectID{0x10}, ObjectID{0x20}
194 body := append(varint(4), varint(4)...)
195 body = append(body, 4, 'a', 'b', 'c', 'd')
196 ea := entryBytes(objRefDelta, len(body), idB[:], body)
197 eb := entryBytes(objRefDelta, len(body), idA[:], body)
198 var offs []uint64
199 pk, idx := buildPack([][]byte{ea, eb}, []ObjectID{idA, idB}, &offs)
200 g := t.TempDir()
201 writeFile(t, g+"/HEAD", "ref: refs/heads/main\n")
202 writeFile(t, g+"/refs/.keep", "")
203 name := fmt.Sprintf("pack-%x", pk[len(pk)-20:])
204 writeFile(t, g+"/objects/pack/"+name+".pack", string(pk))
205 writeFile(t, g+"/objects/pack/"+name+".idx", string(idx))
206 r, err := Open(g)
207 if err != nil {
208 t.Fatal(err)
209 }
210 defer func() { _ = r.Close() }()
211 if _, _, err := r.ReadObject(idA, nil); !errors.Is(err, ErrCorrupt) {
212 t.Fatalf("ReadObject = %v, want ErrCorrupt", err)
213 }
214 if _, _, err := r.ObjectHeader(idA); !errors.Is(err, ErrCorrupt) {
215 t.Fatalf("ObjectHeader = %v, want ErrCorrupt", err)
216 }
217}