1
0
Fork 0
mirror of https://github.com/imjasonh/infinite-git synced 2026-07-08 09:05:06 +00:00
infinite-git/internal/generator/commit.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

191 lines
4.5 KiB
Go

package generator
import (
"fmt"
"sync/atomic"
"time"
"github.com/imjasonh/infinite-git/internal/object"
"github.com/imjasonh/infinite-git/internal/repo"
)
// Generator creates new commits on demand.
type Generator struct {
repo *repo.Repository
counter int64
}
// New creates a new commit generator.
func New(r *repo.Repository) *Generator {
return &Generator{
repo: r,
}
}
// GenerateCommit creates a new commit and updates the main branch.
// It holds the repo lock for the entire read-modify-write cycle to
// prevent concurrent generates from reading the same parent.
func (g *Generator) GenerateCommit() (string, error) {
// Increment counter atomically
count := atomic.AddInt64(&g.counter, 1)
// Hold the repo lock for the entire operation to prevent races.
g.repo.Lock()
defer g.repo.Unlock()
// Get current HEAD commit (use exported method is fine since
// getRefs is called internally, but we already hold the lock,
// so we call the unexported version via GetRefsLocked).
refs, err := g.repo.GetRefsLocked()
if err != nil {
return "", fmt.Errorf("getting refs: %w", err)
}
parentHash := refs["refs/heads/main"]
if parentHash == "" {
return "", fmt.Errorf("main branch not found")
}
// Read parent commit to get its tree
parentData, err := g.repo.ReadObject(parentHash)
if err != nil {
return "", fmt.Errorf("reading parent commit: %w", err)
}
// Parse parent commit to find tree hash
var parentTreeHash string
lines := splitLines(string(parentData))
for _, line := range lines {
if len(line) > 5 && line[:5] == "tree " {
parentTreeHash = line[5:]
break
}
}
// Read parent tree
parentTreeData, err := g.repo.ReadObject(parentTreeHash)
if err != nil {
return "", fmt.Errorf("reading parent tree: %w", err)
}
// Parse existing tree entries
existingEntries := parseTree(parentTreeData)
// Create updated hello.txt content with nanosecond precision
filename := "hello.txt"
content := fmt.Sprintf("Pull #%d\nTimestamp: %s\n", count, time.Now().Format("2006-01-02 15:04:05.999999999"))
// Create blob for updated file
blob := object.NewBlob([]byte(content))
blobHash, err := g.repo.WriteObject(blob)
if err != nil {
return "", fmt.Errorf("writing blob: %w", err)
}
// Create new tree with all existing entries, replacing hello.txt
tree := object.NewTree()
// Add existing entries, but skip hello.txt as we'll add the updated one
for _, entry := range existingEntries {
if entry.Name != filename {
tree.AddEntry(entry.Mode, entry.Name, entry.Hash)
}
}
// Add updated hello.txt
tree.AddEntry("100644", filename, blobHash)
treeHash, err := g.repo.WriteObject(tree)
if err != nil {
return "", fmt.Errorf("writing tree: %w", err)
}
// Create commit
commitMsg := fmt.Sprintf("Pull #%d at %s", count, time.Now().Format("2006-01-02 15:04:05"))
commit := object.NewCommit(
treeHash,
parentHash,
"Infinite Git <infinite@example.com>",
"Infinite Git <infinite@example.com>",
commitMsg,
)
commitHash, err := g.repo.WriteObject(commit)
if err != nil {
return "", fmt.Errorf("writing commit: %w", err)
}
// Update refs/heads/main
if err := g.repo.UpdateRef("refs/heads/main", commitHash); err != nil {
return "", fmt.Errorf("updating ref: %w", err)
}
return commitHash, nil
}
// GetCounter returns the current counter value.
func (g *Generator) GetCounter() int64 {
return atomic.LoadInt64(&g.counter)
}
// splitLines splits a string into lines.
func splitLines(s string) []string {
var lines []string
start := 0
for i, c := range s {
if c == '\n' {
lines = append(lines, s[start:i])
start = i + 1
}
}
if start < len(s) {
lines = append(lines, s[start:])
}
return lines
}
// parseTree parses tree object data into entries.
func parseTree(data []byte) []object.TreeEntry {
var entries []object.TreeEntry
i := 0
for i < len(data) {
// Find space (end of mode)
modeEnd := i
for modeEnd < len(data) && data[modeEnd] != ' ' {
modeEnd++
}
if modeEnd >= len(data) {
break
}
mode := string(data[i:modeEnd])
// Find null (end of name)
nameStart := modeEnd + 1
nameEnd := nameStart
for nameEnd < len(data) && data[nameEnd] != 0 {
nameEnd++
}
if nameEnd >= len(data) {
break
}
name := string(data[nameStart:nameEnd])
// Read 20-byte SHA-1
hashStart := nameEnd + 1
if hashStart+20 > len(data) {
break
}
hash := fmt.Sprintf("%x", data[hashStart:hashStart+20])
entries = append(entries, object.TreeEntry{
Mode: mode,
Name: name,
Hash: hash,
})
i = hashStart + 20
}
return entries
}