main 98594d0

cron.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import (
6	"math"
7	"regexp"
8	"strconv"
9	"strings"
10)
11
12// Cron validation, ported from converter/cron.ts.
13
14type cronRange struct {
15	min, max int
16	names    map[string]int
17}
18
19var (
20	cronMinute = cronRange{0, 59, nil}
21	cronHour   = cronRange{0, 23, nil}
22	cronDOM    = cronRange{1, 31, nil}
23	cronMonth  = cronRange{1, 12, map[string]int{"jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6, "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12}}
24	cronDOW    = cronRange{0, 6, map[string]int{"sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6}}
25)
26
27func isValidCron(cron string) bool {
28	parts := splitSpaces(cron)
29	if len(parts) != 5 {
30		return false
31	}
32	return validateCronPart(parts[0], cronMinute, true) && validateCronPart(parts[1], cronHour, true) &&
33		validateCronPart(parts[2], cronDOM, true) && validateCronPart(parts[3], cronMonth, true) &&
34		validateCronPart(parts[4], cronDOW, true)
35}
36
37// splitSpaces is JavaScript's split(/ +/): runs of spaces separate, other
38// whitespace does not, and leading or trailing spaces give empty parts.
39func splitSpaces(s string) []string {
40	var parts []string
41	start := 0
42	i := 0
43	for i < len(s) {
44		if s[i] != ' ' {
45			i++
46			continue
47		}
48		parts = append(parts, s[start:i])
49		for i < len(s) && s[i] == ' ' {
50			i++
51		}
52		start = i
53	}
54	return append(parts, s[start:])
55}
56
57var jsDecimal = regexp.MustCompile(`^[+-]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$`)
58
59// jsNumber is JavaScript's Number(string): surrounding white space is
60// ignored, an empty string is 0, and decimal, hexadecimal, octal and binary
61// literals and Infinity are numbers. ok is false for NaN.
62func jsNumber(v string) (float64, bool) {
63	t := strings.Trim(v, " \t\n\v\f\r\u00a0\u2028\u2029\ufeff")
64	switch {
65	case t == "":
66		return 0, true
67	case jsDecimal.MatchString(t):
68		f, err := strconv.ParseFloat(t, 64)
69		return f, err == nil || math.IsInf(f, 0)
70	case t == "Infinity" || t == "+Infinity":
71		return math.Inf(1), true
72	case t == "-Infinity":
73		return math.Inf(-1), true
74	case len(t) > 2 && t[0] == '0':
75		base := map[byte]int{'x': 16, 'X': 16, 'o': 8, 'O': 8, 'b': 2, 'B': 2}[t[1]]
76		if base == 0 {
77			return 0, false
78		}
79		n, err := strconv.ParseUint(t[2:], base, 64)
80		if err != nil {
81			if ne, ok := err.(*strconv.NumError); ok && ne.Err == strconv.ErrRange {
82				return math.Inf(1), true
83			}
84			return 0, false
85		}
86		return float64(n), true
87	}
88	return 0, false
89}
90
91func cronNumber(v string) (float64, bool) { return jsNumber(v) }
92
93func validateCronPart(value string, r cronRange, allowSeparators bool) bool {
94	lower := strings.ToLower(value)
95	if _, ok := r.names[lower]; ok {
96		return true
97	}
98	if value == "*" {
99		return true
100	}
101	if strings.Contains(value, ",") {
102		if !allowSeparators {
103			return false
104		}
105		for _, v := range strings.Split(value, ",") {
106			if v == "" || !validateCronPart(v, r, true) {
107				return false
108			}
109		}
110		return true
111	}
112	if strings.Contains(value, "/") {
113		if !allowSeparators {
114			return false
115		}
116		parts := strings.Split(value, "/")
117		start, step := parts[0], ""
118		if len(parts) > 1 {
119			step = parts[1]
120		}
121		n, ok := cronNumber(step)
122		if len(parts) > 2 || !ok || n <= 0 || start == "" || step == "" {
123			return false
124		}
125		return validateCronPart(start, r, true) && validateCronPart(step, r, false)
126	}
127	if strings.Contains(value, "-") {
128		if !allowSeparators {
129			return false
130		}
131		parts := strings.Split(value, "-")
132		if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
133			return false
134		}
135		s, e := cronRangeValue(parts[0], r), cronRangeValue(parts[1], r)
136		return validateCronPart(parts[0], r, false) && validateCronPart(parts[1], r, false) && e >= s
137	}
138	n, ok := cronNumber(value)
139	return ok && n >= float64(r.min) && n <= float64(r.max)
140}
141
142func cronRangeValue(v string, r cronRange) float64 {
143	if n, ok := r.names[strings.ToLower(v)]; ok {
144		return float64(n)
145	}
146	n, ok := cronNumber(v)
147	if !ok {
148		return math.NaN()
149	}
150	return n
151}

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