phase 2 initial test

This commit is contained in:
2026-05-06 23:39:04 +02:00
parent 8592fc5d65
commit 57991e5406
17 changed files with 720 additions and 133 deletions
+49 -22
View File
@@ -2,6 +2,7 @@ package git
import (
"errors"
"fmt"
"os/exec"
"path/filepath"
"strings"
@@ -16,47 +17,69 @@ func SetRepoRoot(root string) {
}
// run executes a git command inside repoPath with strict safety guarantees:
// - repoPath is validated to be under the configured repoRoot
// - args are passed as discrete values — never via shell interpolation
// - the process inherits only a minimal environment
// - repoPath is validated to be under the configured repoRoot (path traversal guard)
// - args are passed as discrete values — never via shell interpolation
// - the process inherits only a minimal, fixed environment
func run(repoPath string, args ...string) ([]byte, error) {
clean := filepath.Clean(repoPath)
if repoRoot != "" && !strings.HasPrefix(clean, repoRoot+string(filepath.Separator)) && clean != repoRoot {
return nil, ErrPathTraversal
if repoRoot != "" {
root := repoRoot + string(filepath.Separator)
if !strings.HasPrefix(clean+string(filepath.Separator), root) && clean != repoRoot {
return nil, ErrPathTraversal
}
}
cmd := exec.Command("git", args...)
cmd.Dir = clean
cmd.Env = []string{
"GIT_TERMINAL_PROMPT=0",
"HOME=" + filepath.Dir(repoRoot), // needed for .gitconfig lookups
"HOME=/tmp", // prevent .gitconfig side-effects
}
return cmd.Output()
out, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("git %s: %w: %s", args[0], err, exitErr.Stderr)
}
return nil, fmt.Errorf("git %s: %w", args[0], err)
}
return out, nil
}
func Init(path string) error {
_, err := run(path, "init", "--bare")
return err
// git init --bare works even if the directory doesn't exist yet
cmd := exec.Command("git", "init", "--bare", path)
cmd.Env = []string{"GIT_TERMINAL_PROMPT=0", "HOME=/tmp"}
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("git init --bare: %w: %s", err, out)
}
return nil
}
type Commit struct {
Hash string
Author string
Message string
Date string
Hash string `json:"hash"`
Author string `json:"author"`
Message string `json:"message"`
Date string `json:"date"`
}
func Log(repoPath, branch string, limit int) ([]Commit, error) {
out, err := run(repoPath, "log", branch,
out, err := run(repoPath,
"log", branch,
"--format=%H\x1f%an\x1f%s\x1f%ci",
"--max-count", strings.TrimSpace(string(rune(limit+'0'))),
fmt.Sprintf("--max-count=%d", limit),
)
if err != nil {
return nil, err
}
raw := strings.TrimSpace(string(out))
if raw == "" {
return nil, nil
}
var commits []Commit
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
parts := strings.Split(line, "\x1f")
for _, line := range strings.Split(raw, "\n") {
parts := strings.SplitN(line, "\x1f", 4)
if len(parts) != 4 {
continue
}
@@ -71,18 +94,22 @@ func Log(repoPath, branch string, limit int) ([]Commit, error) {
}
type TreeEntry struct {
Mode string
Type string
Hash string
Name string
Mode string `json:"mode"`
Type string `json:"type"`
Hash string `json:"hash"`
Name string `json:"name"`
}
func TreeLS(repoPath, ref, subPath string) ([]TreeEntry, error) {
treeRef := ref + ":" + subPath
treeRef := ref
if subPath != "" {
treeRef = ref + ":" + subPath
}
out, err := run(repoPath, "ls-tree", treeRef)
if err != nil {
return nil, err
}
var entries []TreeEntry
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if line == "" {