v2.30.0-pre6 511705f

parse.go RawHistoryFolder

1// Package duration provides human-friendly duration parsing.
2package duration
3
4import (
5	"fmt"
6	"regexp"
7	"strconv"
8	"strings"
9	"time"
10)
11
12// Common duration constants for human-friendly units.
13const (
14	Day   = 24 * time.Hour
15	Week  = 7 * Day
16	Month = 30 * Day  // Approximate
17	Year  = 365 * Day // Approximate
18)
19
20// unitMultipliers maps human-friendly unit suffixes to their duration values.
21var unitMultipliers = map[string]time.Duration{
22	"d": Day,
23	"w": Week,
24	"M": Month, // Capital M for months to distinguish from minutes
25	"y": Year,
26}
27
28// durationPattern matches duration components like "1y", "6M", "2w", "3d".
29var durationPattern = regexp.MustCompile(`(\d+)([ywMd])`)
30
31// Parse extends time.ParseDuration to support human-friendly units:
32//   - d (days) = 24h
33//   - w (weeks) = 7d
34//   - M (months) = 30d (approximate)
35//   - y (years) = 365d (approximate)
36//
37// Standard Go duration units (ns, us, ms, s, m, h) are also supported.
38// Compound durations like "1y6M" or "2w3d" or "1d12h" work as expected.
39//
40// Special case: "0" returns 0 duration (useful for never-expiring tokens).
41//
42// Examples:
43//
44//	Parse("1d")      // 24 hours
45//	Parse("2w")      // 14 days
46//	Parse("1M")      // 30 days
47//	Parse("1y")      // 365 days
48//	Parse("1y6M")    // 1 year and 6 months
49//	Parse("2w3d")    // 2 weeks and 3 days
50//	Parse("1d12h")   // 1 day and 12 hours
51//	Parse("24h")     // standard Go duration
52//	Parse("0")       // zero duration
53func Parse(s string) (time.Duration, error) {
54	s = strings.TrimSpace(s)
55
56	if s == "" {
57		return 0, fmt.Errorf("empty duration string")
58	}
59
60	// Special case: "0" means no expiry
61	if s == "0" {
62		return 0, nil
63	}
64
65	// Check if the string contains any human-friendly units
66	if !containsHumanUnits(s) {
67		// Fall back to standard Go parsing
68		d, err := time.ParseDuration(s)
69		if err != nil {
70			return 0, fmt.Errorf("invalid duration %q: %w (supported units: ns, us, ms, s, m, h, d, w, M, y)", s, err)
71		}
72		return d, nil
73	}
74
75	// Parse human-friendly units
76	return parseHumanDuration(s)
77}
78
79// containsHumanUnits checks if the string contains any human-friendly unit suffixes.
80func containsHumanUnits(s string) bool {
81	for unit := range unitMultipliers {
82		if strings.Contains(s, unit) {
83			return true
84		}
85	}
86	return false
87}
88
89// parseHumanDuration parses a duration string containing human-friendly units.
90func parseHumanDuration(s string) (time.Duration, error) {
91	var total time.Duration
92	remaining := s
93
94	// First, extract all human-friendly units (y, M, w, d)
95	matches := durationPattern.FindAllStringSubmatch(remaining, -1)
96	if len(matches) > 0 {
97		for _, match := range matches {
98			value, err := strconv.ParseInt(match[1], 10, 64)
99			if err != nil {
100				return 0, fmt.Errorf("invalid duration value %q in %q", match[1], s)
101			}
102
103			unit := match[2]
104			multiplier, ok := unitMultipliers[unit]
105			if !ok {
106				return 0, fmt.Errorf("unknown duration unit %q in %q", unit, s)
107			}
108
109			total += time.Duration(value) * multiplier
110		}
111
112		// Remove the matched human-friendly parts
113		remaining = durationPattern.ReplaceAllString(remaining, "")
114	}
115
116	// If there's remaining content, try to parse it as standard Go duration
117	remaining = strings.TrimSpace(remaining)
118	if remaining != "" {
119		d, err := time.ParseDuration(remaining)
120		if err != nil {
121			return 0, fmt.Errorf("invalid duration %q: %w (supported units: ns, us, ms, s, m, h, d, w, M, y)", s, err)
122		}
123		total += d
124	}
125
126	return total, nil
127}

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 7

Tags 106

↑↓ moveenter openesc close