v0.1.0 28f9d6e

ids.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import (
6	"fmt"
7	"strings"
8)
9
10const (
11	idSeparator   = "_"
12	idMaxAttempts = 1000
13	idMaxLength   = 100
14)
15
16// idBuilder is a port of IdBuilder in the reference: it validates the ids
17// written in a file and makes unique ids for steps that have none.
18type idBuilder struct {
19	name     []rune
20	distinct map[string]bool
21}
22
23func newIDBuilder() *idBuilder { return &idBuilder{distinct: map[string]bool{}} }
24
25func isAlpha(c rune) bool   { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' }
26func isNumeric(c rune) bool { return c >= '0' && c <= '9' }
27
28func (b *idBuilder) appendSegment(value string) {
29	if value == "" {
30		return
31	}
32	rs := []rune(value)
33	if len(b.name) == 0 {
34		first := rs[0]
35		switch {
36		case isAlpha(first) || first == '_':
37		case isNumeric(first) || first == '-':
38			b.name = append(b.name, '_')
39		}
40	} else {
41		b.name = append(b.name, '_')
42	}
43	for _, c := range rs {
44		if isAlpha(c) || isNumeric(c) || c == '_' || c == '-' {
45			b.name = append(b.name, c)
46		} else {
47			b.name = append(b.name, '_')
48		}
49	}
50}
51
52// build returns a unique id from the appended segments, "job" when there are
53// none. It returns "" when a thousand ids are taken.
54func (b *idBuilder) build() string {
55	original := "job"
56	if len(b.name) > 0 {
57		original = string(b.name)
58	}
59	for attempt := 1; attempt < idMaxAttempts; attempt++ {
60		suffix := ""
61		if attempt > 1 {
62			suffix = fmt.Sprintf("_%d", attempt)
63		}
64		n := min(len(original), idMaxLength-len(suffix))
65		candidate := original[:n] + suffix
66		if !b.distinct[candidate] {
67			b.distinct[candidate] = true
68			b.name = nil
69			return candidate
70		}
71	}
72	return ""
73}
74
75// tryAddKnownID records an id written in the file. It returns the error
76// message when the id is invalid or already used.
77func (b *idBuilder) tryAddKnownID(value string) string {
78	if value == "" || !validID(value) || len(value) >= idMaxLength {
79		return fmt.Sprintf("The identifier '%s' is invalid. IDs may only contain alphanumeric characters, '_', and '-'. IDs must start with a letter or '_' and and must be less than %d characters.", value, idMaxLength)
80	}
81	if strings.HasPrefix(value, idSeparator+idSeparator) {
82		return fmt.Sprintf("The identifier '%s' is invalid. IDs starting with '__' are reserved.", value)
83	}
84	if b.distinct[value] {
85		return fmt.Sprintf("The identifier '%s' may not be used more than once within the same scope.", value)
86	}
87	b.distinct[value] = true
88	return ""
89}
90
91func validID(name string) bool {
92	for i, c := range name {
93		if i == 0 {
94			if !isAlpha(c) && c != '_' {
95				return false
96			}
97			continue
98		}
99		if !isAlpha(c) && !isNumeric(c) && c != '_' && c != '-' {
100			return false
101		}
102	}
103	return true
104}

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 3

↑↓ moveenter openesc close