1
0
Fork 0
mirror of https://github.com/imjasonh/infinite-git synced 2026-07-08 09:05:06 +00:00
infinite-git/internal/packfile/packfile.go
Claude 29666530cc
Fix four bugs: packfile offset tracking, HTTP error ordering, ref ordering, and race condition
- packfile.go: ReadObject now uses a countingReader to track compressed
  bytes consumed and properly advances r.offset, fixing sequential
  multi-object reads.
- handlers.go: Use commitSHA from GenerateCommit directly instead of
  re-reading refs, which fixes both the HTTP error-after-body-written
  issue and ensures HEAD is always advertised first (Git protocol
  requirement).
- commit.go: GenerateCommit now holds the repo lock for the entire
  read-modify-write cycle, preventing concurrent generates from reading
  the same parent and losing ref updates.
- repo.go: Added Lock/Unlock/GetRefsLocked methods to support holding
  the mutex across multiple repo operations.

https://claude.ai/code/session_015F9BYQoCy2P2zYZBuVeXHH
2026-04-02 05:52:24 +00:00

182 lines
3.8 KiB
Go

package packfile
import (
"bytes"
"compress/zlib"
"crypto/sha1"
"encoding/binary"
"fmt"
"hash"
"io"
)
const (
// Object types in packfile
OBJ_COMMIT = 1
OBJ_TREE = 2
OBJ_BLOB = 3
OBJ_TAG = 4
)
// Writer writes a packfile.
type Writer struct {
buf bytes.Buffer
objects int
hash hash.Hash
}
// NewWriter creates a new packfile writer.
func NewWriter() *Writer {
w := &Writer{
hash: sha1.New(),
}
// Write pack header
w.buf.WriteString("PACK")
binary.Write(&w.buf, binary.BigEndian, uint32(2)) // version
binary.Write(&w.buf, binary.BigEndian, uint32(0)) // placeholder for object count
return w
}
// AddObject adds an object to the packfile.
func (w *Writer) AddObject(objType int, data []byte) error {
w.objects++
// Encode object header
// Format: 1-bit continuation, 3-bit type, 4-bit size (then 7-bit size chunks)
size := len(data)
header := (objType << 4) | (size & 0xf)
size >>= 4
for size > 0 {
header |= 0x80 // Set continuation bit
w.buf.WriteByte(byte(header))
header = size & 0x7f
size >>= 7
}
w.buf.WriteByte(byte(header))
// Compress and write object data
var compressedBuf bytes.Buffer
zw := zlib.NewWriter(&compressedBuf)
if _, err := zw.Write(data); err != nil {
return fmt.Errorf("compressing object: %w", err)
}
if err := zw.Close(); err != nil {
return fmt.Errorf("closing compressor: %w", err)
}
w.buf.Write(compressedBuf.Bytes())
return nil
}
// Finalize completes the packfile and returns the data.
func (w *Writer) Finalize() []byte {
data := w.buf.Bytes()
// Update object count in header
binary.BigEndian.PutUint32(data[8:12], uint32(w.objects))
// Calculate and append checksum
w.hash.Write(data)
checksum := w.hash.Sum(nil)
result := append(data, checksum...)
return result
}
// Reader reads objects from a packfile.
type Reader struct {
data []byte
offset int
}
// NewReader creates a new packfile reader.
func NewReader(data []byte) (*Reader, error) {
if len(data) < 12 {
return nil, fmt.Errorf("packfile too small")
}
if string(data[:4]) != "PACK" {
return nil, fmt.Errorf("invalid packfile signature")
}
version := binary.BigEndian.Uint32(data[4:8])
if version != 2 {
return nil, fmt.Errorf("unsupported packfile version: %d", version)
}
return &Reader{
data: data,
offset: 12, // Skip header
}, nil
}
// readVarint reads a variable-length integer.
func (r *Reader) readVarint() (int, int, error) {
if r.offset >= len(r.data) {
return 0, 0, io.EOF
}
b := r.data[r.offset]
r.offset++
objType := (int(b) >> 4) & 0x7
size := int(b) & 0xf
shift := 4
for b&0x80 != 0 {
if r.offset >= len(r.data) {
return 0, 0, io.EOF
}
b = r.data[r.offset]
r.offset++
size |= (int(b) & 0x7f) << shift
shift += 7
}
return objType, size, nil
}
// ReadObject reads the next object from the packfile.
func (r *Reader) ReadObject() (objType int, data []byte, err error) {
// Read object header
objType, size, err := r.readVarint()
if err != nil {
return 0, nil, err
}
// Wrap the remaining data in a counting reader to track compressed bytes consumed.
cr := &countingReader{reader: bytes.NewReader(r.data[r.offset:])}
zr, err := zlib.NewReader(cr)
if err != nil {
return 0, nil, fmt.Errorf("creating decompressor: %w", err)
}
defer zr.Close()
data = make([]byte, size)
if _, err := io.ReadFull(zr, data); err != nil {
return 0, nil, fmt.Errorf("decompressing object: %w", err)
}
// Drain the zlib reader so cr.n reflects all compressed bytes consumed.
io.Copy(io.Discard, zr)
// Advance offset past the compressed data.
r.offset += int(cr.n)
return objType, data, nil
}
// countingReader wraps an io.Reader and counts bytes read.
type countingReader struct {
reader io.Reader
n int64
}
func (c *countingReader) Read(p []byte) (int, error) {
n, err := c.reader.Read(p)
c.n += int64(n)
return n, err
}