1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package gitcli
4
5import (
6 "bufio"
7 "context"
8 "errors"
9 "fmt"
10 "io"
11 "os/exec"
12 "strings"
13 "time"
14)
15
16// errStop is returned by a pipe consumer that has all it needs: the process is
17// killed and pipe returns nil.
18var errStop = errors.New("stop")
19
20// readArgs returns the arguments of a read-only git command on the bare
21// repository in dir: no replace refs, no optional locks, literal pathspecs and
22// no signature verification, whatever the repository configuration says.
23func readArgs(dir string, args ...string) []string {
24 base := []string{
25 "--no-replace-objects", "--no-optional-locks", "--literal-pathspecs",
26 "-c", "log.showSignature=false",
27 "--git-dir=" + dir,
28 }
29 return append(base, args...)
30}
31
32// pipe runs git and hands its standard output to fn. When fn returns nil the
33// rest of the output is discarded and a failing exit status is an error; when
34// fn returns errStop the process is killed and pipe returns nil; any other
35// error kills the process and is returned. Without a deadline in ctx the
36// process gets commandTimeout.
37func (g *Git) pipe(ctx context.Context, args []string, fn func(*bufio.Reader) error) error {
38 return g.pipeWith(ctx, nil, nil, args, fn)
39}
40
41// pipeWith is pipe with extra environment variables after the isolated ones
42// (the quarantine of a push) and, when stdin is not nil, the standard input.
43func (g *Git) pipeWith(ctx context.Context, extra []string, stdin io.Reader, args []string, fn func(*bufio.Reader) error) error {
44 if _, ok := ctx.Deadline(); !ok {
45 var cancel context.CancelFunc
46 ctx, cancel = context.WithTimeout(ctx, commandTimeout)
47 defer cancel()
48 }
49 release, err := g.acquireExec(ctx)
50 if err != nil {
51 return fmt.Errorf("git %s: %w", subcommand(args), err)
52 }
53 defer release()
54 base := ctx
55 ctx, cancel := context.WithCancel(base)
56 defer cancel()
57
58 cmd := exec.CommandContext(ctx, g.bin, args...)
59 cmd.Env = append(append(make([]string, 0, len(g.env)+len(extra)), g.env...), extra...)
60 cmd.Dir = g.dataDir
61 cmd.WaitDelay = 5 * time.Second
62 cmd.Stdin = stdin
63 stderr := &tailBuffer{max: 4096}
64 cmd.Stderr = stderr
65 stdout, err := cmd.StdoutPipe()
66 if err != nil {
67 return fmt.Errorf("git %s: %w", subcommand(args), err)
68 }
69 if err := cmd.Start(); err != nil {
70 return fmt.Errorf("git %s: %w", subcommand(args), err)
71 }
72 r := bufio.NewReaderSize(stdout, 64*1024)
73 ferr := fn(r)
74 if ferr == nil {
75 _, ferr = io.Copy(io.Discard, r)
76 }
77 if ferr != nil {
78 cancel()
79 }
80 werr := cmd.Wait()
81
82 switch {
83 case errors.Is(ferr, errStop):
84 return nil
85 case base.Err() != nil:
86 return fmt.Errorf("git %s: %w", subcommand(args), base.Err())
87 case ferr != nil:
88 return ferr
89 case werr != nil:
90 if msg := stderr.String(); msg != "" {
91 return fmt.Errorf("git %s: %w: %s", subcommand(args), werr, msg)
92 }
93 return fmt.Errorf("git %s: %w", subcommand(args), werr)
94 }
95 return nil
96}
97
98// subcommand returns the git subcommand of args for error messages: the first
99// argument that is neither an option nor the value of -c or -C.
100func subcommand(args []string) string {
101 for i := 0; i < len(args); i++ {
102 a := args[i]
103 switch {
104 case a == "-c":
105 i++
106 case strings.HasPrefix(a, "-"):
107 default:
108 return a
109 }
110 }
111 return "?"
112}
113
114// readToken reads up to the next NUL and returns the text before it.
115func readToken(r *bufio.Reader) (string, error) {
116 s, err := r.ReadString(0)
117 if err != nil {
118 if errors.Is(err, io.EOF) && s != "" {
119 return "", io.ErrUnexpectedEOF
120 }
121 return "", err
122 }
123 return s[:len(s)-1], nil
124}
125
126// lineReader reads text lines, keeping at most max bytes of each: the rest of
127// a longer line is dropped, so a pathological line cannot use unbounded memory.
128type lineReader struct {
129 r *bufio.Reader
130 max int
131 buf []byte
132}
133
134func newLineReader(r *bufio.Reader, max int) *lineReader {
135 return &lineReader{r: r, max: max}
136}
137
138// next returns the next line without its "\n", or io.EOF. cut is true when
139// the line was longer than max and the end of it was dropped. The slice is
140// valid until the next call.
141func (l *lineReader) next() (line []byte, cut bool, err error) {
142 l.buf = l.buf[:0]
143 got := false
144 for {
145 chunk, err := l.r.ReadSlice('\n')
146 if len(chunk) > 0 {
147 got = true
148 }
149 data := chunk
150 if err == nil {
151 data = chunk[:len(chunk)-1]
152 }
153 room := l.max - len(l.buf)
154 if len(data) > room {
155 cut = true
156 }
157 if room > 0 {
158 l.buf = append(l.buf, data[:min(room, len(data))]...)
159 }
160 switch {
161 case err == nil:
162 return l.buf, cut, nil
163 case errors.Is(err, bufio.ErrBufferFull):
164 case errors.Is(err, io.EOF):
165 if !got {
166 return nil, false, io.EOF
167 }
168 return l.buf, cut, nil
169 default:
170 return nil, false, err
171 }
172 }
173}