This repository has no description
10 kB
386 lines
1// NOTE: lot's of code compied from Gitea with slight modification to use go-git objects
2
3package gitea
4
5import (
6 "bufio"
7 "bytes"
8 "context"
9 "fmt"
10 "io"
11 "math"
12 "os/exec"
13 "strconv"
14 "strings"
15
16 "github.com/djherbis/buffer"
17 "github.com/djherbis/nio/v3"
18 "github.com/go-git/go-git/v5/plumbing"
19 "github.com/go-git/go-git/v5/plumbing/filemode"
20 "github.com/go-git/go-git/v5/plumbing/hash"
21 "github.com/go-git/go-git/v5/plumbing/object"
22)
23
24func GetCommit(ctx context.Context, repoPath, rev string) (*object.Commit, error) {
25 wr, rd, cancel := CatFileBatch(ctx, repoPath)
26 defer cancel()
27
28 if _, err := wr.Write([]byte(rev + "\n")); err != nil {
29 return nil, fmt.Errorf("write rev: %w", err)
30 }
31 sha, typ, size, err := ReadBatchLine(rd)
32 if err != nil {
33 return nil, err
34 }
35 if typ != "commit" {
36 if err := DiscardFull(rd, size+1); err != nil {
37 return nil, err
38 }
39 return nil, fmt.Errorf("unexpected type: %s for commit: %s", typ, rev)
40 }
41 commit, err := ReadCommit(plumbing.NewHash(string(sha)), io.LimitReader(rd, size))
42 if err != nil {
43 return nil, fmt.Errorf("read commit %s: %w", rev, err)
44 }
45 if _, err := rd.Discard(1); err != nil {
46 return nil, err
47 }
48 return commit, nil
49}
50
51func GetTree(ctx context.Context, repoPath, rev string) (*object.Tree, error) {
52 bw, br, cancel := CatFileBatch(ctx, repoPath)
53 defer cancel()
54
55 return BatchGetTree(bw, br, rev)
56}
57
58func BatchGetTree(bw io.WriteCloser, br *bufio.Reader, rev string) (*object.Tree, error) {
59 if _, err := bw.Write([]byte(rev + "\n")); err != nil {
60 return nil, fmt.Errorf("write rev: %w", err)
61 }
62 sha, typ, size, err := ReadBatchLine(br)
63 if err != nil {
64 return nil, fmt.Errorf("resolve %s: %w", rev, err)
65 }
66 if typ != "tree" {
67 if err := DiscardFull(br, size+1); err != nil {
68 return nil, err
69 }
70 return nil, fmt.Errorf("unexpected type: %s for tree: %s", typ, rev)
71 }
72
73 entries, err := catBatchParseTreeEntries(br, size)
74 if err != nil {
75 return nil, fmt.Errorf("read tree %s: %w", rev, err)
76 }
77 return &object.Tree{
78 Hash: plumbing.NewHash(string(sha)),
79 Entries: entries,
80 }, nil
81}
82
83func catBatchParseTreeEntries(rd *bufio.Reader, sz int64) ([]object.TreeEntry, error) {
84 entries := make([]object.TreeEntry, 0, 10)
85loop:
86 for sz > 0 {
87 mode, fname, sha, count, err := ParseCatFileTreeLine(rd)
88 if err != nil {
89 if err == io.EOF {
90 break loop
91 }
92 return nil, err
93 }
94 modeNum, err := strconv.ParseUint(string(mode), 8, 32)
95 if err != nil {
96 return nil, err
97 }
98 sz -= int64(count)
99 entry := object.TreeEntry{
100 Name: string(fname),
101 Mode: filemode.FileMode(modeNum),
102 Hash: plumbing.Hash(sha),
103 }
104 entries = append(entries, entry)
105 }
106 if _, err := rd.Discard(1); err != nil {
107 return entries, err
108 }
109 return entries, nil
110}
111
112func CatFileBatchCheck(ctx context.Context, repoPath string) (io.WriteCloser, *bufio.Reader, func()) {
113 batchStdinReader, batchStdinWriter := io.Pipe()
114 batchStdoutReader, batchStdoutWriter := nio.Pipe(buffer.New(32 * 1024))
115 ctx, ctxCancel := context.WithCancel(ctx)
116 closed := make(chan struct{})
117 cancel := func() {
118 ctxCancel()
119 _ = batchStdinWriter.Close()
120 _ = batchStdoutReader.Close()
121 <-closed
122 }
123
124 // Ensure cancel is called as soon as the provided context is cancelled
125 go func() {
126 <-ctx.Done()
127 cancel()
128 }()
129
130 go func() {
131 stderr := &strings.Builder{}
132 cmd := exec.CommandContext(ctx, "git", "-C", repoPath, "cat-file", "--batch-check")
133 cmd.Stdin = batchStdinReader
134 cmd.Stdout = batchStdoutWriter
135 cmd.Stderr = stderr
136 if err := cmd.Run(); err != nil {
137 _ = batchStdinReader.CloseWithError(fmt.Errorf("%w\n%s", err, stderr.String()))
138 _ = batchStdoutWriter.CloseWithError(fmt.Errorf("%w\n%s", err, stderr.String()))
139 } else {
140 _ = batchStdoutWriter.Close()
141 _ = batchStdinReader.Close()
142 }
143 close(closed)
144 }()
145
146 batchReader := bufio.NewReaderSize(batchStdoutReader, 32*1024)
147 return batchStdinWriter, batchReader, cancel
148}
149
150func CatFileBatch(ctx context.Context, repoPath string) (io.WriteCloser, *bufio.Reader, func()) {
151 batchStdinReader, batchStdinWriter := io.Pipe()
152 batchStdoutReader, batchStdoutWriter := nio.Pipe(buffer.New(32 * 1024))
153 ctx, ctxCancel := context.WithCancel(ctx)
154 closed := make(chan struct{})
155 cancel := func() {
156 ctxCancel()
157 _ = batchStdinWriter.Close()
158 _ = batchStdoutReader.Close()
159 <-closed
160 }
161
162 // Ensure cancel is called as soon as the provided context is cancelled
163 go func() {
164 <-ctx.Done()
165 cancel()
166 }()
167
168 go func() {
169 stderr := &strings.Builder{}
170 cmd := exec.CommandContext(ctx, "git", "-C", repoPath, "cat-file", "--batch")
171 cmd.Stdin = batchStdinReader
172 cmd.Stdout = batchStdoutWriter
173 cmd.Stderr = stderr
174 if err := cmd.Run(); err != nil {
175 _ = batchStdinReader.CloseWithError(fmt.Errorf("%w\n%s", err, stderr.String()))
176 _ = batchStdoutWriter.CloseWithError(fmt.Errorf("%w\n%s", err, stderr.String()))
177 } else {
178 _ = batchStdoutWriter.Close()
179 _ = batchStdinReader.Close()
180 }
181 close(closed)
182 }()
183
184 batchReader := bufio.NewReaderSize(batchStdoutReader, 32*1024)
185 return batchStdinWriter, batchReader, cancel
186}
187
188func ReadBatchLine(reader io.Reader) (sha []byte, typ string, size int64, err error) {
189 rd, ok := reader.(*bufio.Reader)
190 if !ok {
191 rd = bufio.NewReader(reader)
192 }
193 typ, err = rd.ReadString('\n')
194 if err != nil {
195 return sha, typ, size, err
196 }
197 if len(typ) == 1 {
198 typ, err = rd.ReadString('\n')
199 if err != nil {
200 return sha, typ, size, err
201 }
202 }
203 idx := strings.IndexByte(typ, ' ')
204 if idx < 0 {
205 return sha, typ, size, fmt.Errorf("missing sha: %s", sha)
206 }
207 sha = []byte(typ[:idx])
208 typ = typ[idx+1:]
209
210 idx = strings.IndexByte(typ, ' ')
211 if idx < 0 {
212 return sha, typ, size, fmt.Errorf("missing size: %s", sha)
213 }
214
215 sizeStr := typ[idx+1 : len(typ)-1]
216 typ = typ[:idx]
217
218 size, err = strconv.ParseInt(sizeStr, 10, 64)
219 return sha, typ, size, err
220}
221
222// NOTE: readCommit doesn't return complete go-git [object.Commit] object!
223// The embedded object store is missing, so calling method from returned commit
224// can lead to panic.
225func ReadCommit(oid plumbing.Hash, reader io.Reader) (*object.Commit, error) {
226 commit := &object.Commit{
227 Hash: oid,
228 ExtraHeaders: make(map[string][]byte),
229 }
230
231 bufReader := bufio.NewReader(reader)
232 inHeader := true
233 var payloadSB, messageSB bytes.Buffer
234 var headerKey string
235 var headerValue []byte
236 for {
237 line, err := bufReader.ReadBytes('\n')
238 if err != nil && err != io.EOF {
239 return nil, err
240 }
241 if len(line) == 0 {
242 break
243 }
244
245 if inHeader {
246 inHeader = !(len(line) == 1 && line[0] == '\n') // a bare newline ends the header block
247 k, v, _ := bytes.Cut(line, []byte{' '})
248 if len(k) != 0 || !inHeader {
249 if headerKey != "" {
250 assignCommitHeader(commit, headerKey, headerValue)
251 }
252 headerKey = string(k) // also resets headerValue via the assignment below
253 headerValue = v
254 } else {
255 headerValue = append(headerValue, v...)
256 }
257 if headerKey != "gpgsig" && headerKey != "gpgsig-sha256" {
258 _, _ = payloadSB.Write(line)
259 }
260 } else {
261 _, _ = messageSB.Write(line)
262 _, _ = payloadSB.Write(line)
263 }
264
265 if err == io.EOF {
266 break
267 }
268 }
269
270 commit.Message = messageSB.String()
271 // TODO: pass raw payload so we can verify it without reconstructing the payload
272 // if commit.Signature != nil {
273 // commit.Signature.Payload = payloadSB.String()
274 // }
275 return commit, nil
276}
277
278func assignCommitHeader(commit *object.Commit, headerKey string, headerValue []byte) {
279 value := bytes.TrimSuffix(headerValue, []byte{'\n'})
280 switch headerKey {
281 case "tree":
282 commit.TreeHash = plumbing.NewHash(string(value))
283 case "parent":
284 commit.ParentHashes = append(commit.ParentHashes, plumbing.NewHash(string(value)))
285 case "author":
286 commit.Author.Decode(value)
287 case "committer":
288 commit.Committer.Decode(value)
289 case "gpgsig", "gpgsig-sha256":
290 // if there are duplicate "gpgsig" and "gpgsig-sha256" headers, then the signature must have already been invalid
291 // so we don't need to handle duplicate headers here
292 commit.PGPSignature = string(value)
293 case "mergetag":
294 commit.MergeTag = string(value)
295 default:
296 commit.ExtraHeaders[headerKey] = value
297 }
298}
299
300// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream
301// This carefully avoids allocations - except where fnameBuf is too small.
302// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
303//
304// Each line is composed of:
305// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH>
306//
307// We don't attempt to convert the raw HASH to save a lot of time
308func ParseCatFileTreeLine(rd *bufio.Reader) (mode, fname, sha []byte, n int, err error) {
309 modeBuf := make([]byte, 40)
310 fnameBuf := make([]byte, 4096)
311 shaBuf := make([]byte, hash.HexSize)
312
313 var readBytes []byte
314
315 // Read the Mode & fname
316 readBytes, err = rd.ReadSlice('\x00')
317 if err != nil {
318 return mode, fname, sha, n, err
319 }
320 idx := bytes.IndexByte(readBytes, ' ')
321 if idx < 0 {
322 return mode, fname, sha, n, fmt.Errorf("missing")
323 }
324
325 n += idx + 1
326 copy(modeBuf, readBytes[:idx])
327 if len(modeBuf) >= idx {
328 modeBuf = modeBuf[:idx]
329 } else {
330 modeBuf = append(modeBuf, readBytes[len(modeBuf):idx]...)
331 }
332 mode = modeBuf
333
334 readBytes = readBytes[idx+1:]
335
336 // Deal with the fname
337 copy(fnameBuf, readBytes)
338 if len(fnameBuf) > len(readBytes) {
339 fnameBuf = fnameBuf[:len(readBytes)]
340 } else {
341 fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...)
342 }
343 for err == bufio.ErrBufferFull {
344 readBytes, err = rd.ReadSlice('\x00')
345 fnameBuf = append(fnameBuf, readBytes...)
346 }
347 n += len(fnameBuf)
348 if err != nil {
349 return mode, fname, sha, n, err
350 }
351 fnameBuf = fnameBuf[:len(fnameBuf)-1]
352 fname = fnameBuf
353
354 // Deal with the binary hash
355 idx = 0
356 length := hash.HexSize / 2
357 for idx < length {
358 var read int
359 read, err = rd.Read(shaBuf[idx:length])
360 n += read
361 if err != nil {
362 return mode, fname, sha, n, err
363 }
364 idx += read
365 }
366 sha = shaBuf
367 return mode, fname, sha, n, err
368}
369
370func DiscardFull(rd *bufio.Reader, discard int64) error {
371 if discard > math.MaxInt32 {
372 n, err := rd.Discard(math.MaxInt32)
373 discard -= int64(n)
374 if err != nil {
375 return err
376 }
377 }
378 for discard > 0 {
379 n, err := rd.Discard(int(discard))
380 discard -= int64(n)
381 if err != nil {
382 return err
383 }
384 }
385 return nil
386}