main 5004296

git.go RawHistoryFolder

1package main
2
3import (
4	"bytes"
5	"context"
6	"fmt"
7	"os"
8	"os/exec"
9	"regexp"
10	"strings"
11	"time"
12)
13
14// Fetcher is the network/git boundary of wlbsync.
15type Fetcher interface {
16	// Resolve returns the ref and commit for src. An empty pin selects the
17	// default ref of the source (latest tag or branch HEAD); otherwise pin is
18	// a tag, a branch, or a 40-hex commit.
19	Resolve(src upstream, pin string) (ref, commit string, err error)
20	// Checkout materialises commit of src (reached through ref) in the empty
21	// directory dir.
22	Checkout(src upstream, ref, commit, dir string) error
23}
24
25var fullSHA = regexp.MustCompile(`^[0-9a-f]{40}$`)
26
27// gitFetcher implements Fetcher with the git command line.
28type gitFetcher struct{}
29
30const gitTimeout = 5 * time.Minute
31
32func git(dir string, args ...string) (string, error) {
33	ctx, cancel := context.WithTimeout(context.Background(), gitTimeout)
34	defer cancel()
35	cmd := exec.CommandContext(ctx, "git", args...)
36	cmd.Dir = dir
37	cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
38	var stdout, stderr bytes.Buffer
39	cmd.Stdout, cmd.Stderr = &stdout, &stderr
40	if err := cmd.Run(); err != nil {
41		return "", fmt.Errorf("git %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(stderr.String()))
42	}
43	return strings.TrimSpace(stdout.String()), nil
44}
45
46// lsRemote lists the tags (peeled to commits) and branches of url.
47func lsRemote(url string) (tags, heads map[string]string, err error) {
48	out, err := git("", "ls-remote", "--tags", "--heads", url)
49	if err != nil {
50		return nil, nil, err
51	}
52	tags, heads = parseLsRemote(out)
53	return tags, heads, nil
54}
55
56// parseLsRemote parses `git ls-remote --tags --heads` output. Annotated tags
57// report the tag object and a peeled "^{}" line; the peeled commit wins.
58func parseLsRemote(out string) (tags, heads map[string]string) {
59	tags, heads = map[string]string{}, map[string]string{}
60	for _, line := range strings.Split(out, "\n") {
61		sha, name, ok := strings.Cut(strings.TrimSpace(line), "\t")
62		if !ok {
63			continue
64		}
65		switch {
66		case strings.HasPrefix(name, "refs/tags/"):
67			tag := strings.TrimPrefix(name, "refs/tags/")
68			if t, peeled := strings.CutSuffix(tag, "^{}"); peeled {
69				tags[t] = sha
70			} else if _, seen := tags[t]; !seen {
71				tags[t] = sha
72			}
73		case strings.HasPrefix(name, "refs/heads/"):
74			heads[strings.TrimPrefix(name, "refs/heads/")] = sha
75		}
76	}
77	return tags, heads
78}
79
80// latestTag returns the highest tag of src accepted by its tag filters.
81func latestTag(src upstream, tags map[string]string) (string, error) {
82	best := ""
83	for t := range tags {
84		if src.TagRe != nil && !src.TagRe.MatchString(t) {
85			continue
86		}
87		if src.TagOK != nil && !src.TagOK(t) {
88			continue
89		}
90		if best == "" || compareVersions(t, best) > 0 {
91			best = t
92		}
93	}
94	if best == "" {
95		return "", fmt.Errorf("%s: no tag matches the release pattern", src.ID)
96	}
97	return best, nil
98}
99
100// Resolve implements Fetcher.
101func (gitFetcher) Resolve(src upstream, pin string) (string, string, error) {
102	if fullSHA.MatchString(pin) {
103		if src.Kind == PinBranch {
104			return src.Branch, pin, nil // recorded as the branch, at the pinned commit
105		}
106		return pin, pin, nil
107	}
108	tags, heads, err := lsRemote(src.URL)
109	if err != nil {
110		return "", "", fmt.Errorf("%s: %w", src.ID, err)
111	}
112	if src.Kind == PinBranch {
113		branch := src.Branch
114		if pin != "" {
115			branch = pin
116		}
117		sha, ok := heads[branch]
118		if !ok {
119			return "", "", fmt.Errorf("%s: branch %q not found", src.ID, branch)
120		}
121		return branch, sha, nil
122	}
123	ref := pin
124	if ref == "" {
125		if ref, err = latestTag(src, tags); err != nil {
126			return "", "", err
127		}
128	}
129	sha, ok := tags[ref]
130	if !ok {
131		return "", "", fmt.Errorf("%s: tag %q not found", src.ID, ref)
132	}
133	return ref, sha, nil
134}
135
136// Checkout implements Fetcher: a shallow clone of the tag or branch, falling
137// back to a shallow fetch of the commit when the branch moved past a pinned
138// commit or the ref is itself a commit. HEAD is verified against commit.
139func (gitFetcher) Checkout(src upstream, ref, commit, dir string) error {
140	if !fullSHA.MatchString(ref) {
141		_, err := git("", "clone", "-q", "--depth", "1", "--branch", ref, src.URL, dir)
142		if err != nil {
143			return fmt.Errorf("%s: %w", src.ID, err)
144		}
145		if verifyHead(src, dir, commit) == nil {
146			return nil
147		}
148		if src.Kind != PinBranch {
149			return verifyHead(src, dir, commit)
150		}
151		// The branch moved since the commit was resolved or pinned.
152		if err := os.RemoveAll(dir); err != nil {
153			return err
154		}
155		if err := os.MkdirAll(dir, 0o755); err != nil {
156			return err
157		}
158	}
159	if err := checkoutCommit(src.URL, commit, dir); err != nil {
160		return fmt.Errorf("%s: %w", src.ID, err)
161	}
162	return nil
163}
164
165func checkoutCommit(url, commit, dir string) error {
166	steps := [][]string{
167		{"init", "-q"},
168		{"remote", "add", "origin", url},
169		{"fetch", "-q", "--depth", "1", "origin", commit},
170		{"-c", "advice.detachedHead=false", "checkout", "-q", "FETCH_HEAD"},
171	}
172	for _, s := range steps {
173		if _, err := git(dir, s...); err != nil {
174			return err
175		}
176	}
177	return verifyHead(upstream{ID: url}, dir, commit)
178}
179
180func verifyHead(src upstream, dir, commit string) error {
181	head, err := git(dir, "rev-parse", "HEAD")
182	if err != nil {
183		return err
184	}
185	if head != commit {
186		return fmt.Errorf("%s: checked out %s, expected %s (ref moved during sync?)", src.ID, head, commit)
187	}
188	return nil
189}

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 1

Tags 1

↑↓ moveenter openesc close