detached 973e682

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	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}

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 2

Tags 45

↑↓ moveenter openesc close