mirror of
https://github.com/imjasonh/infinite-git
synced 2026-07-08 09:05:06 +00:00
- 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
182 lines
3.8 KiB
Go
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
|
|
}
|