detached fda3c2b

reference.go RawHistoryFolder

1package domain
2
3import (
4	"regexp"
5	"slices"
6	"strconv"
7)
8
9// MaxReferenceTextBytes bounds each title, description, note and commit message scanned.
10const MaxReferenceTextBytes = 16 << 10
11
12var numberRef = regexp.MustCompile(`(^|[^0-9A-Za-z_&#])#([1-9][0-9]{0,9})\b`)
13var closingRef = regexp.MustCompile(`(?i)(?:^|[^0-9A-Za-z_])(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)(?::[ \t]*|[ \t]+)#([1-9][0-9]{0,9})\b((?:[ \t]*,[ \t]*#[1-9][0-9]{0,9}\b|[ \t]+and[ \t]+#[1-9][0-9]{0,9}\b)*)`)
14
15// FindReferences returns the positive #N numbers in text, sorted and unique.
16// Numbers glued to words, entities or another # are excluded, as are zero,
17// leading zeroes and numbers longer than ten digits. Code is scanned as text;
18// cross-repository references and !N are not supported.
19func FindReferences(text string) []int64 {
20	numbers := make(map[int64]struct{})
21	// Resume at the end of the number, retaining its following delimiter for the
22	// next match (for example, both references in "#1,#2").
23	for offset := 0; offset < len(text); {
24		match := numberRef.FindStringSubmatchIndex(text[offset:])
25		if match == nil {
26			break
27		}
28		start, end := offset+match[4], offset+match[5]
29		// A sliced search must not manufacture a beginning-of-text boundary.
30		if start < 2 || !referenceWord(text[start-2]) {
31			n, _ := strconv.ParseInt(text[start:end], 10, 64)
32			numbers[n] = struct{}{}
33		}
34		offset = end
35	}
36	return sortedReferences(numbers)
37}
38
39func referenceWord(b byte) bool {
40	return b >= '0' && b <= '9' || b >= 'A' && b <= 'Z' || b >= 'a' && b <= 'z' || b == '_' || b == '&' || b == '#'
41}
42
43// FindClosingReferences returns sorted, unique references following close,
44// closes, closed, fix, fixes, fixed, resolve, resolves or resolved (case
45// insensitive). A colon is optional; lists use commas or "and", not spaces.
46// Like FindReferences, this scans code spans and fences as ordinary text.
47func FindClosingReferences(text string) []int64 {
48	numbers := make(map[int64]struct{})
49	for _, match := range closingRef.FindAllString(text, -1) {
50		for _, n := range FindReferences(match) {
51			numbers[n] = struct{}{}
52		}
53	}
54	return sortedReferences(numbers)
55}
56
57func sortedReferences(numbers map[int64]struct{}) []int64 {
58	var result []int64
59	for n := range numbers {
60		result = append(result, n)
61	}
62	slices.Sort(result)
63	return result
64}
65
66// MergeRequestReferences describes the issues named by a merge request.
67type MergeRequestReferences struct {
68	Mentions []int64
69	Closes   []int64
70}
71
72// ScanMergeRequest scans titles, descriptions, notes and commit messages for
73// references. Notes can mention issues but cannot close them. Each individual
74// text is truncated to MaxReferenceTextBytes, without a trailing partial token, before scanning; no Markdown or
75// cross-repository interpretation is performed.
76func ScanMergeRequest(title, description string, notes, commitMessages []string) MergeRequestReferences {
77	mentions, closes := make(map[int64]struct{}), make(map[int64]struct{})
78	scan := func(text string, closing bool) {
79		if len(text) > MaxReferenceTextBytes {
80			// Drop the trailing partial token: back off to the last ASCII
81			// whitespace at or before the limit.
82			cut := -1
83			for i := MaxReferenceTextBytes; i >= 0; i-- {
84				if i < len(text) && (text[i] == ' ' || text[i] == '\t' || text[i] == '\n' || text[i] == '\r') {
85					cut = i
86					break
87				}
88			}
89			if cut < 0 {
90				return
91			}
92			text = text[:cut]
93		}
94		for _, n := range FindReferences(text) {
95			mentions[n] = struct{}{}
96		}
97		if closing {
98			for _, n := range FindClosingReferences(text) {
99				closes[n] = struct{}{}
100			}
101		}
102	}
103	scan(title, true)
104	scan(description, true)
105	for _, text := range notes {
106		scan(text, false)
107	}
108	for _, text := range commitMessages {
109		scan(text, true)
110	}
111	return MergeRequestReferences{Mentions: sortedReferences(mentions), Closes: sortedReferences(closes)}
112}

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