File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/gitx/gitx.go
352 lines · 10kb · 133728eaa05486504991b230f9d4c7e986b7defc
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
// Package gitx runs git with argument arrays and never a shell, because ref names are untrusted.
133728e barerepo 1mo
2
package gitx
133728e barerepo 1mo
3
133728e barerepo 1mo
4
import (
133728e barerepo 1mo
5
"bytes"
133728e barerepo 1mo
6
"context"
133728e barerepo 1mo
7
"encoding/hex"
133728e barerepo 1mo
8
"fmt"
133728e barerepo 1mo
9
"io"
133728e barerepo 1mo
10
"io/fs"
133728e barerepo 1mo
11
"os"
133728e barerepo 1mo
12
"os/exec"
133728e barerepo 1mo
13
"path/filepath"
133728e barerepo 1mo
14
"strconv"
133728e barerepo 1mo
15
"strings"
133728e barerepo 1mo
16
)
133728e barerepo 1mo
17
133728e barerepo 1mo
18
// Bin is the git executable. Resolved once at start.
133728e barerepo 1mo
19
var Bin = "git"
133728e barerepo 1mo
20
133728e barerepo 1mo
21
// Identity signs the commits the server makes itself, and every install wants its own host in it.
133728e barerepo 1mo
22
var Identity = "barerepo@localhost"
133728e barerepo 1mo
23
133728e barerepo 1mo
24
// Run executes git in dir and returns stdout. Args are passed as an array.
133728e barerepo 1mo
25
func Run(ctx context.Context, dir string, args ...string) (string, error) {
133728e barerepo 1mo
26
var out, errb bytes.Buffer
133728e barerepo 1mo
27
cmd := exec.CommandContext(ctx, Bin, args...)
133728e barerepo 1mo
28
cmd.Dir = dir
133728e barerepo 1mo
29
cmd.Stdout = &out
133728e barerepo 1mo
30
cmd.Stderr = &errb
133728e barerepo 1mo
31
cmd.Env = env()
133728e barerepo 1mo
32
if err := cmd.Run(); err != nil {
133728e barerepo 1mo
33
msg := strings.TrimSpace(errb.String())
133728e barerepo 1mo
34
if msg == "" {
133728e barerepo 1mo
35
msg = err.Error()
133728e barerepo 1mo
36
}
133728e barerepo 1mo
37
return out.String(), fmt.Errorf("git %s: %s", args[0], msg)
133728e barerepo 1mo
38
}
133728e barerepo 1mo
39
return out.String(), nil
133728e barerepo 1mo
40
}
133728e barerepo 1mo
41
133728e barerepo 1mo
42
// RunStdin executes git with input on stdin, which is how a blob is written without a file.
133728e barerepo 1mo
43
func RunStdin(ctx context.Context, dir, stdin string, args ...string) (string, error) {
133728e barerepo 1mo
44
var out, errb bytes.Buffer
133728e barerepo 1mo
45
cmd := exec.CommandContext(ctx, Bin, args...)
133728e barerepo 1mo
46
cmd.Dir = dir
133728e barerepo 1mo
47
cmd.Stdin = strings.NewReader(stdin)
133728e barerepo 1mo
48
cmd.Stdout = &out
133728e barerepo 1mo
49
cmd.Stderr = &errb
133728e barerepo 1mo
50
cmd.Env = env()
133728e barerepo 1mo
51
if err := cmd.Run(); err != nil {
133728e barerepo 1mo
52
msg := strings.TrimSpace(errb.String())
133728e barerepo 1mo
53
if msg == "" {
133728e barerepo 1mo
54
msg = err.Error()
133728e barerepo 1mo
55
}
133728e barerepo 1mo
56
return out.String(), fmt.Errorf("git %s: %s", args[0], msg)
133728e barerepo 1mo
57
}
133728e barerepo 1mo
58
return out.String(), nil
133728e barerepo 1mo
59
}
133728e barerepo 1mo
60
133728e barerepo 1mo
61
// Pipe wires stdin and stdout to the caller, which is how upload-pack and receive-pack are served.
133728e barerepo 1mo
62
func Pipe(ctx context.Context, dir string, stdin io.Reader, stdout, stderr io.Writer, extraEnv []string, args ...string) error {
133728e barerepo 1mo
63
cmd := exec.CommandContext(ctx, Bin, args...)
133728e barerepo 1mo
64
cmd.Dir = dir
133728e barerepo 1mo
65
cmd.Stdin = stdin
133728e barerepo 1mo
66
cmd.Stdout = stdout
133728e barerepo 1mo
67
cmd.Stderr = stderr
133728e barerepo 1mo
68
cmd.Env = append(env(), extraEnv...)
133728e barerepo 1mo
69
return cmd.Run()
133728e barerepo 1mo
70
}
133728e barerepo 1mo
71
133728e barerepo 1mo
72
// env is built from nothing, so a user's own git configuration cannot change what the server reads.
133728e barerepo 1mo
73
func env() []string {
133728e barerepo 1mo
74
e := []string{
133728e barerepo 1mo
75
"GIT_CONFIG_NOSYSTEM=1",
133728e barerepo 1mo
76
"HOME=/nonexistent",
133728e barerepo 1mo
77
"GIT_TERMINAL_PROMPT=0",
133728e barerepo 1mo
78
"PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin",
133728e barerepo 1mo
79
"LC_ALL=C",
133728e barerepo 1mo
80
// git spells a path outside ascii as octal escapes unless told not to, and barerepo parses paths.
133728e barerepo 1mo
81
"GIT_CONFIG_COUNT=3",
133728e barerepo 1mo
82
"GIT_CONFIG_KEY_0=core.quotePath",
133728e barerepo 1mo
83
"GIT_CONFIG_VALUE_0=false",
133728e barerepo 1mo
84
"GIT_CONFIG_KEY_1=core.fsync",
133728e barerepo 1mo
85
"GIT_CONFIG_VALUE_1=objects,derived-metadata,reference",
133728e barerepo 1mo
86
"GIT_CONFIG_KEY_2=core.fsyncMethod",
133728e barerepo 1mo
87
"GIT_CONFIG_VALUE_2=batch",
133728e barerepo 1mo
88
// git needs an identity, and chapter 10 gives an account no address to borrow.
133728e barerepo 1mo
89
"GIT_AUTHOR_NAME=barerepo",
133728e barerepo 1mo
90
"GIT_AUTHOR_EMAIL=" + Identity,
133728e barerepo 1mo
91
"GIT_COMMITTER_NAME=barerepo",
133728e barerepo 1mo
92
"GIT_COMMITTER_EMAIL=" + Identity,
133728e barerepo 1mo
93
}
133728e barerepo 1mo
94
for _, k := range passthrough {
133728e barerepo 1mo
95
if v, ok := os.LookupEnv(k); ok {
133728e barerepo 1mo
96
e = append(e, k+"="+v)
133728e barerepo 1mo
97
}
133728e barerepo 1mo
98
}
133728e barerepo 1mo
99
return e
133728e barerepo 1mo
100
}
133728e barerepo 1mo
101
133728e barerepo 1mo
102
// passthrough is what lets a hook see the quarantined objects the push is still delivering.
133728e barerepo 1mo
103
var passthrough = []string{
133728e barerepo 1mo
104
"GIT_DIR",
133728e barerepo 1mo
105
"GIT_OBJECT_DIRECTORY",
133728e barerepo 1mo
106
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
133728e barerepo 1mo
107
"GIT_QUARANTINE_PATH",
133728e barerepo 1mo
108
}
133728e barerepo 1mo
109
133728e barerepo 1mo
110
// Version fails if git is missing, at start, rather than during a user's first clone.
133728e barerepo 1mo
111
func Version(ctx context.Context) (string, error) {
133728e barerepo 1mo
112
path, err := exec.LookPath(Bin)
133728e barerepo 1mo
113
if err != nil {
133728e barerepo 1mo
114
return "", fmt.Errorf("git not found in PATH")
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
Bin = path
133728e barerepo 1mo
117
out, err := Run(ctx, "", "version")
133728e barerepo 1mo
118
return strings.TrimSpace(out), err
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
133728e barerepo 1mo
121
// Object is one object read by CatFile.
133728e barerepo 1mo
122
type Object struct {
133728e barerepo 1mo
123
SHA string
133728e barerepo 1mo
124
Type string
133728e barerepo 1mo
125
Size int64
133728e barerepo 1mo
126
Body string
133728e barerepo 1mo
127
}
133728e barerepo 1mo
128
133728e barerepo 1mo
129
// CatFile gets the hash, type, size and content in one process, because chapter 25 counts processes.
133728e barerepo 1mo
130
func CatFile(ctx context.Context, dir, spec string) (*Object, error) {
133728e barerepo 1mo
131
out, err := RunStdin(ctx, dir, spec+"\n", "cat-file", "--batch")
133728e barerepo 1mo
132
if err != nil {
133728e barerepo 1mo
133
return nil, err
133728e barerepo 1mo
134
}
133728e barerepo 1mo
135
header, body, found := strings.Cut(out, "\n")
133728e barerepo 1mo
136
if !found {
133728e barerepo 1mo
137
return nil, fmt.Errorf("cat-file gave no answer for %q", spec)
133728e barerepo 1mo
138
}
133728e barerepo 1mo
139
fields := strings.Fields(header)
133728e barerepo 1mo
140
if len(fields) < 3 {
133728e barerepo 1mo
141
// "<spec> missing" is what git says for something that is not there.
133728e barerepo 1mo
142
return nil, fmt.Errorf("no object at %q", spec)
133728e barerepo 1mo
143
}
133728e barerepo 1mo
144
size, err := strconv.ParseInt(fields[2], 10, 64)
133728e barerepo 1mo
145
if err != nil {
133728e barerepo 1mo
146
return nil, fmt.Errorf("cat-file gave a size of %q", fields[2])
133728e barerepo 1mo
147
}
133728e barerepo 1mo
148
if int64(len(body)) > size {
133728e barerepo 1mo
149
body = body[:size]
133728e barerepo 1mo
150
}
133728e barerepo 1mo
151
return &Object{SHA: fields[0], Type: fields[1], Size: size, Body: body}, nil
133728e barerepo 1mo
152
}
133728e barerepo 1mo
153
133728e barerepo 1mo
154
// HeadBranch reads the HEAD file, because rule 6 says ask the repository, not start a process.
133728e barerepo 1mo
155
func HeadBranch(dir string) (string, error) {
133728e barerepo 1mo
156
body, err := os.ReadFile(filepath.Join(dir, "HEAD"))
133728e barerepo 1mo
157
if err != nil {
133728e barerepo 1mo
158
return "", err
133728e barerepo 1mo
159
}
133728e barerepo 1mo
160
ref, ok := strings.CutPrefix(strings.TrimSpace(string(body)), "ref: refs/heads/")
133728e barerepo 1mo
161
if !ok {
133728e barerepo 1mo
162
// An object id means a detached HEAD, so this repository was not made by barerepo.
133728e barerepo 1mo
163
return "", fmt.Errorf("HEAD is not on a branch")
133728e barerepo 1mo
164
}
133728e barerepo 1mo
165
return ref, nil
133728e barerepo 1mo
166
}
133728e barerepo 1mo
167
133728e barerepo 1mo
168
// symbolicDepth bounds the chase, because a ref file pointing at itself is a file somebody wrote.
133728e barerepo 1mo
169
const symbolicDepth = 5
133728e barerepo 1mo
170
133728e barerepo 1mo
171
// ResolveRef reads the ref file or packed-refs, saving the process chapter 25 counts.
133728e barerepo 1mo
172
func ResolveRef(dir, ref string) (string, error) {
133728e barerepo 1mo
173
for i := 0; i < symbolicDepth; i++ {
133728e barerepo 1mo
174
sha, err := resolveOnce(dir, ref)
133728e barerepo 1mo
175
if err != nil {
133728e barerepo 1mo
176
return "", err
133728e barerepo 1mo
177
}
133728e barerepo 1mo
178
// HEAD holds "ref: refs/heads/master", so returning the file is returning a name, not a hash.
133728e barerepo 1mo
179
next, symbolic := strings.CutPrefix(sha, "ref: ")
133728e barerepo 1mo
180
if !symbolic {
133728e barerepo 1mo
181
return sha, nil
133728e barerepo 1mo
182
}
133728e barerepo 1mo
183
ref = strings.TrimSpace(next)
133728e barerepo 1mo
184
}
133728e barerepo 1mo
185
return "", fmt.Errorf("%s points through more than %d refs", ref, symbolicDepth)
133728e barerepo 1mo
186
}
133728e barerepo 1mo
187
133728e barerepo 1mo
188
// resolveOnce reads one ref file, following nothing.
133728e barerepo 1mo
189
func resolveOnce(dir, ref string) (string, error) {
133728e barerepo 1mo
190
if !ValidRef(ref) {
133728e barerepo 1mo
191
return "", fmt.Errorf("%q is not a ref name", ref)
133728e barerepo 1mo
192
}
133728e barerepo 1mo
193
if body, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(ref))); err == nil {
133728e barerepo 1mo
194
return strings.TrimSpace(string(body)), nil
133728e barerepo 1mo
195
}
133728e barerepo 1mo
196
packed, err := os.ReadFile(filepath.Join(dir, "packed-refs"))
133728e barerepo 1mo
197
if err != nil {
133728e barerepo 1mo
198
return "", err
133728e barerepo 1mo
199
}
133728e barerepo 1mo
200
for _, line := range strings.Split(string(packed), "\n") {
133728e barerepo 1mo
201
if line == "" || line[0] == '#' || line[0] == '^' {
133728e barerepo 1mo
202
continue
133728e barerepo 1mo
203
}
133728e barerepo 1mo
204
sha, name, ok := strings.Cut(line, " ")
133728e barerepo 1mo
205
if ok && name == ref {
133728e barerepo 1mo
206
return sha, nil
133728e barerepo 1mo
207
}
133728e barerepo 1mo
208
}
133728e barerepo 1mo
209
return "", fmt.Errorf("no ref named %s", ref)
133728e barerepo 1mo
210
}
133728e barerepo 1mo
211
133728e barerepo 1mo
212
// ResolveRefOrAsk falls back to git, because reading the file is wrong inside a worktree.
133728e barerepo 1mo
213
func ResolveRefOrAsk(ctx context.Context, dir, ref string) (string, error) {
133728e barerepo 1mo
214
if sha, err := ResolveRef(dir, ref); err == nil {
133728e barerepo 1mo
215
return sha, nil
133728e barerepo 1mo
216
}
133728e barerepo 1mo
217
out, err := Run(ctx, dir, "rev-parse", "--verify", "--quiet", ref)
133728e barerepo 1mo
218
if err != nil {
218 of 352 lines · the whole file
history · rawbarerepo 0.1.0