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 if _, ok := ctx.Deadline(); !ok {
39 var cancel context.CancelFunc
40 ctx, cancel = context.WithTimeout(ctx, commandTimeout)
41 defer cancel()
42 }
43 release, err := g.acquireExec(ctx)
44 if err != nil {
45 return fmt.Errorf("git %s: %w", subcommand(args), err)
46 }
47 defer release()
48 base := ctx
49 ctx, cancel := context.WithCancel(base)
50 defer cancel()
51
52 cmd := exec.CommandContext(ctx, g.bin, args...)
53 cmd.Env = g.env
54 cmd.Dir = g.dataDir
55 cmd.WaitDelay = 5 * time.Second
56 stderr := &tailBuffer{max: 4096}
57 cmd.Stderr = stderr
58 stdout, err := cmd.StdoutPipe()
59 if err != nil {
60 return fmt.Errorf("git %s: %w", subcommand(args), err)
61 }
62 if err := cmd.Start(); err != nil {
63 return fmt.Errorf("git %s: %w", subcommand(args), err)
64 }
65 r := bufio.NewReaderSize(stdout, 64*1024)
66 ferr := fn(r)
67 if ferr == nil {
68 _, ferr = io.Copy(io.Discard, r)
69 }
70 if ferr != nil {
71 cancel()
72 }
73 werr := cmd.Wait()
74
75 switch {
76 case errors.Is(ferr, errStop):
77 return nil
78 case base.Err() != nil:
79 return fmt.Errorf("git %s: %w", subcommand(args), base.Err())
80 case ferr != nil:
81 return ferr
82 case werr != nil:
83 if msg := stderr.String(); msg != "" {
84 return fmt.Errorf("git %s: %w: %s", subcommand(args), werr, msg)
85 }
86 return fmt.Errorf("git %s: %w", subcommand(args), werr)
87 }
88 return nil
89}
90
91// subcommand returns the git subcommand of args for error messages: the first
92// argument that is neither an option nor the value of -c or -C.
93func subcommand(args []string) string {
94 for i := 0; i < len(args); i++ {
95 a := args[i]
96 switch {
97 case a == "-c":
98 i++
99 case strings.HasPrefix(a, "-"):
100 default:
101 return a
102 }
103 }
104 return "?"
105}
106
107// readToken reads up to the next NUL and returns the text before it.
108func readToken(r *bufio.Reader) (string, error) {
109 s, err := r.ReadString(0)
110 if err != nil {
111 if errors.Is(err, io.EOF) && s != "" {
112 return "", io.ErrUnexpectedEOF
113 }
114 return "", err
115 }
116 return s[:len(s)-1], nil
117}
118
119// lineReader reads text lines, keeping at most max bytes of each: the rest of
120// a longer line is dropped, so a pathological line cannot use unbounded memory.
121type lineReader struct {
122 r *bufio.Reader
123 max int
124 buf []byte
125}
126
127func newLineReader(r *bufio.Reader, max int) *lineReader {
128 return &lineReader{r: r, max: max}
129}
130
131// next returns the next line without its "\n", or io.EOF. cut is true when
132// the line was longer than max and the end of it was dropped. The slice is
133// valid until the next call.
134func (l *lineReader) next() (line []byte, cut bool, err error) {
135 l.buf = l.buf[:0]
136 got := false
137 for {
138 chunk, err := l.r.ReadSlice('\n')
139 if len(chunk) > 0 {
140 got = true
141 }
142 data := chunk
143 if err == nil {
144 data = chunk[:len(chunk)-1]
145 }
146 room := l.max - len(l.buf)
147 if len(data) > room {
148 cut = true
149 }
150 if room > 0 {
151 l.buf = append(l.buf, data[:min(room, len(data))]...)
152 }
153 switch {
154 case err == nil:
155 return l.buf, cut, nil
156 case errors.Is(err, bufio.ErrBufferFull):
157 case errors.Is(err, io.EOF):
158 if !got {
159 return nil, false, io.EOF
160 }
161 return l.buf, cut, nil
162 default:
163 return nil, false, err
164 }
165 }
166}