This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knotmirror / xrpc / gitea / batch.go
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}