v0.11.0 0633e44

pipe.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 44

↑↓ moveenter openesc close