repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/gitrepo/gitrepo.go
Rawpackage gitrepo
import (
"bytes"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
func RepoPath(roots []string, repo string) (string, error) {
if !cleanRepo(repo) {
return "", fmt.Errorf("unsafe repo %q", repo)
}
for _, root := range roots {
for _, name := range []string{repo, repo + ".git"} {
path := filepath.Join(root, name)
if isGitDir(path) {
return path, nil
}
}
}
return "", fmt.Errorf("repo %q not found", repo)
}
func Resolve(repoPath, ref, rev string) (string, error) {
if strings.TrimSpace(rev) != "" {
return gitOutput("--git-dir", repoPath, "rev-parse", "--verify", "--end-of-options", rev+"^{commit}")
}
if strings.TrimSpace(ref) == "" {
return "", fmt.Errorf("ref or rev required")
}
return gitOutput("--git-dir", repoPath, "rev-parse", "--verify", "--end-of-options", ref+"^{commit}")
}
func DefaultBranch(repoPath string) (string, string, error) {
ref, err := gitOutput("--git-dir", repoPath, "symbolic-ref", "HEAD")
if err != nil {
return "", "", err
}
if !strings.HasPrefix(ref, "refs/heads/") {
return "", "", fmt.Errorf("default ref %q is not a branch", ref)
}
rev, err := Resolve(repoPath, ref, "")
return ref, rev, err
}
func ChangedPaths(repoPath, oldRev, newRev string) ([]string, error) {
if !objectID(newRev) || (strings.Trim(oldRev, "0") != "" && !objectID(oldRev)) {
return nil, fmt.Errorf("changed paths require object IDs")
}
var args []string
if strings.Trim(oldRev, "0") == "" {
args = []string{"--git-dir", repoPath, "ls-tree", "-r", "--name-only", "-z", newRev}
} else {
args = []string{"--git-dir", repoPath, "diff", "--name-only", "--no-renames", "-z", oldRev, newRev, "--"}
}
cmd := exec.Command("git", args...)
var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("git changed paths: %w: %s", err, strings.TrimSpace(stderr.String()))
}
data := stdout.Bytes()
if len(data) == 0 {
return nil, nil
}
parts := bytes.Split(data[:len(data)-1], []byte{0})
paths := make([]string, len(parts))
for i, part := range parts {
paths[i] = string(part)
}
return paths, nil
}
var mkdirAll = os.MkdirAll
func Checkout(repoPath, rev, workspace string) error {
if !objectID(rev) {
return fmt.Errorf("valid revision required")
}
if err := os.RemoveAll(workspace); err != nil {
return err
}
if err := mkdirAll(workspace, 0o755); err != nil {
return err
}
absoluteRepoPath, err := filepath.Abs(repoPath)
if err != nil {
return err
}
if out, err := exec.Command("git", "init", "--quiet", workspace).CombinedOutput(); err != nil {
return fmt.Errorf("git init: %w: %s", err, strings.TrimSpace(string(out)))
}
repoURL := (&url.URL{Scheme: "file", Path: absoluteRepoPath}).String()
cmd := exec.Command("git", "-C", workspace, "fetch", "--quiet", "--depth=1", "--no-tags", repoURL, rev)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("git fetch: %w: %s", err, strings.TrimSpace(string(out)))
}
cmd = exec.Command("git", "-C", workspace, "checkout", "--quiet", "--detach", "FETCH_HEAD")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("git checkout: %w: %s", err, strings.TrimSpace(string(out)))
}
return nil
}
func objectID(value string) bool {
if len(value) != 40 && len(value) != 64 {
return false
}
_, err := strconv.ParseUint(value[:16], 16, 64)
if err != nil {
return false
}
for _, char := range value[16:] {
if !strings.ContainsRune("0123456789abcdefABCDEF", char) {
return false
}
}
return true
}
func cleanRepo(repo string) bool {
return strings.TrimSpace(repo) != "" && repo == filepath.Base(repo) && repo != "." && repo != ".." && !strings.Contains(repo, string(filepath.Separator))
}
func isGitDir(path string) bool {
if info, err := os.Stat(filepath.Join(path, "HEAD")); err == nil && !info.IsDir() {
return true
}
return false
}
func gitOutput(args ...string) (string, error) {
cmd := exec.Command("git", args...)
out, err := cmd.CombinedOutput()
text := strings.TrimSpace(string(out))
if err != nil {
return "", fmt.Errorf("git %s: %w: %s", strings.Join(args, " "), err, text)
}
return text, nil
}
// Subject returns the one-line commit subject of a revision.
func Subject(repoPath, rev string) string {
if !objectID(rev) {
return ""
}
subject, err := gitOutput("--git-dir", repoPath, "log", "-1", "--format=%s", rev)
if err != nil {
return ""
}
return subject
}
// FilesAtRev returns the contents of every file under a tree directory at a
// revision, keyed by path relative to that directory.
func FilesAtRev(repoPath, rev, dir string) (map[string]string, error) {
if !objectID(rev) {
return nil, fmt.Errorf("valid revision required")
}
listing, err := gitOutput("--git-dir", repoPath, "ls-tree", "-r", "--name-only", rev, "--", dir)
if err != nil {
return nil, err
}
files := map[string]string{}
for _, path := range strings.Split(listing, "\n") {
path = strings.TrimSpace(path)
if path == "" {
continue
}
content, err := gitOutput("--git-dir", repoPath, "show", rev+":"+path)
if err != nil {
return nil, err
}
files[strings.TrimPrefix(strings.TrimPrefix(path, dir), "/")] = content
}
return files, nil
}