package git import ( "bytes" "fmt" "os/exec" "path/filepath" "strings" ) // Repo wraps a bare directory that is a Git repository. type Repo struct { root string } // Open returns a Repo for an existing directory, initialising Git if needed. func Open(root string) (*Repo, error) { r := &Repo{root: root} if err := r.initIfNeeded(); err != nil { return nil, err } return r, nil } func (r *Repo) initIfNeeded() error { out, _ := r.run("rev-parse", "--is-inside-work-tree") if strings.TrimSpace(out) == "true" { return nil } _, err := r.run("init") return err } // Commit writes data to path and creates a Git commit authored by author. func (r *Repo) Commit(path, content, message, author, email string) error { full := filepath.Join(r.root, path) if err := writeFile(full, content); err != nil { return err } if _, err := r.run("add", path); err != nil { return err } _, err := r.run( "-c", fmt.Sprintf("user.name=%s", author), "-c", fmt.Sprintf("user.email=%s", email), "commit", "-m", message, ) return err } // Log returns the commit history for a file. func (r *Repo) Log(path string) ([]LogEntry, error) { out, err := r.run("log", "--format=%H|%an|%ae|%ai|%s", "--", path) if err != nil { return nil, err } var entries []LogEntry for _, line := range strings.Split(strings.TrimSpace(out), "\n") { if line == "" { continue } parts := strings.SplitN(line, "|", 5) if len(parts) == 5 { entries = append(entries, LogEntry{ Hash: parts[0], Author: parts[1], Email: parts[2], Date: parts[3], Subject: parts[4], }) } } return entries, nil } // Diff returns the unified diff between two commits for a file. func (r *Repo) Diff(path, fromHash, toHash string) (string, error) { out, err := r.run("diff", fromHash, toHash, "--", path) return out, err } // Show returns the file content at a specific commit. func (r *Repo) Show(hash, path string) (string, error) { return r.run("show", fmt.Sprintf("%s:%s", hash, path)) } type LogEntry struct { Hash string Author string Email string Date string Subject string } func (r *Repo) run(args ...string) (string, error) { cmd := exec.Command("git", append([]string{"-C", r.root}, args...)...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return "", fmt.Errorf("git %v: %w — %s", args, err, stderr.String()) } return stdout.String(), nil }