114 lines
2.5 KiB
Go
114 lines
2.5 KiB
Go
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)
|
|
}
|