internal/gitutil/gitutil.go
102 lines · 3109 bytes
1// Package gitutil wraps the system git binary. All repository access goes
2// through git subprocesses; there is no in-process git implementation.
3package gitutil
4
5import (
6 "fmt"
7 "io"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strings"
12)
13
14// InitBare creates a bare repository with the shared hooks directory wired
15// via core.hooksPath.
16func InitBare(path, defaultBranch, hooksPath string) error {
17 if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
18 return err
19 }
20 cmd := exec.Command("git", "init", "--bare", "--initial-branch="+defaultBranch, path)
21 if out, err := cmd.CombinedOutput(); err != nil {
22 return fmt.Errorf("git init: %v\n%s", err, out)
23 }
24 cmd = exec.Command("git", "-C", path, "config", "core.hooksPath", hooksPath)
25 if out, err := cmd.CombinedOutput(); err != nil {
26 return fmt.Errorf("git config core.hooksPath: %v\n%s", err, out)
27 }
28 return nil
29}
30
31// Transport streams one git transport service (upload-pack, receive-pack,
32// upload-archive) over rw. extraEnv entries are appended to the daemon's
33// environment; hooks read the FORGE_* variables from it.
34func Transport(service, repoPath string, rw io.ReadWriter, errW io.Writer, extraEnv []string) error {
35 var args []string
36 switch service {
37 case "git-upload-pack", "git-receive-pack", "git-upload-archive":
38 args = []string{strings.TrimPrefix(service, "git-"), repoPath}
39 default:
40 return fmt.Errorf("unknown service %q", service)
41 }
42 cmd := exec.Command("git", args...)
43 cmd.Env = append(os.Environ(), extraEnv...)
44 cmd.Stdin = rw
45 cmd.Stdout = rw
46 cmd.Stderr = errW
47 return cmd.Run()
48}
49
50// IsAncestor reports whether old is an ancestor of new in the repository at
51// dir. It must run with the caller's environment intact so that quarantined
52// objects during pre-receive remain visible.
53func IsAncestor(dir, old, new string) (bool, error) {
54 cmd := exec.Command("git", "-C", dir, "merge-base", "--is-ancestor", old, new)
55 err := cmd.Run()
56 if err == nil {
57 return true, nil
58 }
59 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
60 return false, nil
61 }
62 return false, err
63}
64
65// ZeroSHA reports whether s is an all-zero object id (SHA-1 or SHA-256).
66func ZeroSHA(s string) bool {
67 if len(s) != 40 && len(s) != 64 {
68 return false
69 }
70 for i := 0; i < len(s); i++ {
71 if s[i] != '0' {
72 return false
73 }
74 }
75 return true
76}
77
78// RevList returns up to limit commit SHAs reachable from ref, newest first.
79func RevList(dir, ref string, limit int) ([]string, error) {
80 cmd := exec.Command("git", "-C", dir, "rev-list", fmt.Sprintf("--max-count=%d", limit), ref)
81 out, err := cmd.Output()
82 if err != nil {
83 return nil, fmt.Errorf("rev-list %s: %w", ref, err)
84 }
85 var shas []string
86 for _, l := range strings.Split(strings.TrimSpace(string(out)), "\n") {
87 if l != "" {
88 shas = append(shas, l)
89 }
90 }
91 return shas, nil
92}
93
94// ReadCommit returns the raw commit object bytes.
95func ReadCommit(dir, sha string) ([]byte, error) {
96 cmd := exec.Command("git", "-C", dir, "cat-file", "commit", sha)
97 out, err := cmd.Output()
98 if err != nil {
99 return nil, fmt.Errorf("cat-file commit %s: %w", sha, err)
100 }
101 return out, nil
102}