git.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3// Package gitcli implements the Git ports over the git binary. Every
4// invocation runs in an isolated environment that is built here and never
5// inherits the caller's.
6package gitcli
7
8import (
9	"bytes"
10	"context"
11	"errors"
12	"fmt"
13	"io"
14	"os/exec"
15	"path/filepath"
16	"runtime"
17	"strings"
18	"sync"
19	"sync/atomic"
20	"time"
21)
22
23// Config configures a Git adapter. The adapter never reads the process
24// environment: the caller passes what it needs.
25type Config struct {
26	// Bin is the git executable: a name looked up in the process PATH, or a path.
27	// It is required; the default belongs to the configuration.
28	Bin string
29	// DataDir is the data root (absolute). repos/, repos/.staging/ and
30	// trash/ below it must exist; run/git-home/ is the HOME of git.
31	DataDir string
32	// Path is the PATH given to git, which it uses to find its helpers.
33	Path string
34	// MaxReceiveBytes is the largest pack a push may send
35	// (receive.maxInputSize); zero leaves Git unlimited.
36	MaxReceiveBytes int64
37	// CatFileIdle is how long a repository's long-lived `git cat-file`
38	// process may stay unused before it is stopped. Zero means 60 seconds.
39	CatFileIdle time.Duration
40	// MaxCatFiles bounds the long-lived cat-file processes. Zero means 16.
41	MaxCatFiles int
42	// MaxExec bounds the one-shot git processes that run at once (every
43	// command but the long-lived cat-file processes and the blob streams);
44	// callers beyond it wait for a slot until their context ends. Zero means
45	// max(4, 2*GOMAXPROCS).
46	MaxExec int
47	// MaxBlobStreams bounds the open blob streams, each of which holds a git
48	// process until its reader is closed. Streams beyond it are refused with
49	// domain.ErrResourceExhausted rather than queued: a slow reader could hold
50	// a slot for long. Zero means 32.
51	MaxBlobStreams int
52}
53
54// Git is the git CLI adapter for ports.GitRepos.
55type Git struct {
56	bin     string
57	dataDir string
58	env     []string
59	maxRecv int64
60
61	mu      sync.Mutex // guards backend
62	backend string     // git-http-backend, resolved on first use
63
64	cat *catPool // long-lived cat-file processes of the read side
65
66	onTrash atomic.Pointer[func(dir string)] // see OnTrash
67
68	exec  chan struct{} // slots of the one-shot processes, see Config.MaxExec
69	blobs chan struct{} // slots of the blob streams, see Config.MaxBlobStreams
70}
71
72// Defaults of the process bounds.
73const defaultMaxBlobStreams = 32
74
75func defaultMaxExec() int { return max(4, 2*runtime.GOMAXPROCS(0)) }
76
77// New builds an adapter. It does not run git: call CheckVersion.
78func New(cfg Config) *Git {
79	env := buildEnv(cfg.DataDir, cfg.Path)
80	maxExec, maxBlobs := cfg.MaxExec, cfg.MaxBlobStreams
81	if maxExec <= 0 {
82		maxExec = defaultMaxExec()
83	}
84	if maxBlobs <= 0 {
85		maxBlobs = defaultMaxBlobStreams
86	}
87	return &Git{
88		bin: cfg.Bin, dataDir: cfg.DataDir, env: env, maxRecv: cfg.MaxReceiveBytes,
89		cat:   newCatPool(cfg.Bin, env, cfg.CatFileIdle, cfg.MaxCatFiles),
90		exec:  make(chan struct{}, maxExec),
91		blobs: make(chan struct{}, maxBlobs),
92	}
93}
94
95// Close stops the long-lived git processes of the read side and waits for
96// them, until ctx ends. Reads after Close fail with ErrClosed. Close is
97// idempotent; the adapter's write side keeps working.
98func (g *Git) Close(ctx context.Context) error { return g.cat.close(ctx) }
99
100// buildEnv returns the complete environment of every git invocation. Nothing
101// else is passed: no GIT_* variable, no user HOME, no locale.
102func buildEnv(dataDir, path string) []string {
103	return []string{
104		"GIT_CONFIG_NOSYSTEM=1",
105		"HOME=" + filepath.Join(dataDir, "run", "git-home"),
106		"GIT_TERMINAL_PROMPT=0",
107		"LC_ALL=C",
108		"PATH=" + path,
109	}
110}
111
112// commandTimeout bounds one git invocation when ctx has no deadline.
113const commandTimeout = 2 * time.Minute
114
115// acquireExec takes a slot of the one-shot processes, waiting until ctx ends.
116// The wait counts against the deadline of ctx, so a saturated server answers a
117// request with its deadline rather than hanging it.
118func (g *Git) acquireExec(ctx context.Context) (release func(), err error) {
119	select {
120	case g.exec <- struct{}{}:
121		return func() { <-g.exec }, nil
122	case <-ctx.Done():
123		return nil, ctx.Err()
124	}
125}
126
127// run executes git with args in the isolated environment and returns stdout.
128func (g *Git) run(ctx context.Context, args ...string) (string, error) {
129	return g.runInput(ctx, nil, args...)
130}
131
132// runInput is run with the given standard input; nil means none.
133func (g *Git) runInput(ctx context.Context, stdin io.Reader, args ...string) (string, error) {
134	if _, ok := ctx.Deadline(); !ok {
135		var cancel context.CancelFunc
136		ctx, cancel = context.WithTimeout(ctx, commandTimeout)
137		defer cancel()
138	}
139	release, err := g.acquireExec(ctx)
140	if err != nil {
141		return "", fmt.Errorf("git %s: %w", subcommand(args), err)
142	}
143	defer release()
144	cmd := exec.CommandContext(ctx, g.bin, args...)
145	cmd.Env = g.env
146	cmd.Dir = g.dataDir
147	cmd.WaitDelay = 5 * time.Second
148	cmd.Stdin = stdin
149	var stdout, stderr bytes.Buffer
150	cmd.Stdout = &stdout
151	cmd.Stderr = &stderr
152	if err := cmd.Run(); err != nil {
153		msg := strings.TrimSpace(stderr.String())
154		if len(msg) > 512 {
155			msg = msg[:512] + "…"
156		}
157		var exit *exec.ExitError
158		if errors.As(err, &exit) && msg != "" {
159			return "", fmt.Errorf("git %s: %w: %s", subcommand(args), err, msg)
160		}
161		return "", fmt.Errorf("git %s: %w", subcommand(args), err)
162	}
163	return stdout.String(), nil
164}

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