first round of files

This commit is contained in:
2026-05-06 23:13:06 +02:00
parent a30962474d
commit 2aa5d01307
24 changed files with 991 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
package git
import "strings"
// ParseAGitRef extracts target and source branch from an AGit push ref.
// AGit refs follow the pattern: refs/for/<target>[/<source>]
// Returns ("", "", false) if the ref is not an AGit ref.
func ParseAGitRef(ref string) (target, source string, ok bool) {
const prefix = "refs/for/"
if !strings.HasPrefix(ref, prefix) {
return "", "", false
}
rest := strings.TrimPrefix(ref, prefix)
parts := strings.SplitN(rest, "/", 2)
target = parts[0]
if len(parts) == 2 {
source = parts[1]
} else {
source = target
}
return target, source, true
}
+113
View File
@@ -0,0 +1,113 @@
package git
import (
"errors"
"os/exec"
"path/filepath"
"strings"
)
var ErrPathTraversal = errors.New("repo path outside of configured root")
var repoRoot string
func SetRepoRoot(root string) {
repoRoot = filepath.Clean(root)
}
// 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
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
}
cmd := exec.Command("git", args...)
cmd.Dir = clean
cmd.Env = []string{
"GIT_TERMINAL_PROMPT=0",
"HOME=" + filepath.Dir(repoRoot), // needed for .gitconfig lookups
}
return cmd.Output()
}
func Init(path string) error {
_, err := run(path, "init", "--bare")
return err
}
type Commit struct {
Hash string
Author string
Message string
Date string
}
func Log(repoPath, branch string, limit int) ([]Commit, error) {
out, err := run(repoPath, "log", branch,
"--format=%H\x1f%an\x1f%s\x1f%ci",
"--max-count", strings.TrimSpace(string(rune(limit+'0'))),
)
if err != nil {
return nil, err
}
var commits []Commit
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
parts := strings.Split(line, "\x1f")
if len(parts) != 4 {
continue
}
commits = append(commits, Commit{
Hash: parts[0],
Author: parts[1],
Message: parts[2],
Date: parts[3],
})
}
return commits, nil
}
type TreeEntry struct {
Mode string
Type string
Hash string
Name string
}
func TreeLS(repoPath, ref, subPath string) ([]TreeEntry, error) {
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 == "" {
continue
}
// format: <mode> SP <type> SP <hash> TAB <name>
tabIdx := strings.Index(line, "\t")
if tabIdx < 0 {
continue
}
name := line[tabIdx+1:]
fields := strings.Fields(line[:tabIdx])
if len(fields) != 3 {
continue
}
entries = append(entries, TreeEntry{
Mode: fields[0],
Type: fields[1],
Hash: fields[2],
Name: name,
})
}
return entries, nil
}
func BlobCat(repoPath, ref, filePath string) ([]byte, error) {
return run(repoPath, "show", ref+":"+filePath)
}