v0.16.0 924eaca

name.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6	"fmt"
7	"strings"
8)
9
10// MaxRepoNameLen is the longest repository name, in characters.
11const MaxRepoNameLen = 100
12
13// gitSuffix is the suffix of a Git URL and of a repository directory.
14const gitSuffix = ".git"
15
16// reservedNames cannot name a repository: they are routes of the server.
17var reservedNames = map[string]bool{
18	"-":           true,
19	"robots.txt":  true,
20	"favicon.ico": true,
21}
22
23// RepoName is a validated repository name. Only ParseRepoName and
24// ParseRepoPath build a non-zero value; the zero value is invalid.
25type RepoName struct{ s string }
26
27// String returns the name.
28func (n RepoName) String() string { return n.s }
29
30// IsZero reports whether n is the invalid zero value.
31func (n RepoName) IsZero() bool { return n.s == "" }
32
33// ParseRepoName validates s as a repository name. It does not strip any
34// ".git" suffix: a name ending in ".git" is invalid.
35//
36// The error matches ErrInvalidName and is an *InvalidNameError.
37func ParseRepoName(s string) (RepoName, error) {
38	reason := nameProblem(s)
39	if reason == "" {
40		return RepoName{s: s}, nil
41	}
42	e := &InvalidNameError{Name: s, Reason: reason}
43	if lower := strings.ToLower(s); lower != s && nameProblem(lower) == "" {
44		e.Suggestion = lower
45	}
46	return RepoName{}, e
47}
48
49// ParseRepoPath parses the repository segment of a URL path. It strips one
50// trailing ".git" before validating, so "tools.git" names "tools".
51func ParseRepoPath(segment string) (RepoName, error) {
52	return ParseRepoName(strings.TrimSuffix(segment, gitSuffix))
53}
54
55// InvalidNameError describes why a repository name is invalid. It matches
56// ErrInvalidName.
57type InvalidNameError struct {
58	Name       string
59	Reason     string
60	Suggestion string // a valid alternative, empty when there is none
61}
62
63// Error implements error.
64func (e *InvalidNameError) Error() string {
65	msg := fmt.Sprintf("invalid repository name %q: %s", e.Name, e.Reason)
66	if e.Suggestion != "" {
67		msg += fmt.Sprintf(" (try %q)", e.Suggestion)
68	}
69	return msg
70}
71
72// Unwrap makes errors.Is(err, ErrInvalidName) true.
73func (e *InvalidNameError) Unwrap() error { return ErrInvalidName }
74
75// nameProblem returns why s is not a valid name, or "" when it is valid.
76func nameProblem(s string) string {
77	if s == "" {
78		return "name is empty"
79	}
80	if len(s) > MaxRepoNameLen {
81		return fmt.Sprintf("name is longer than %d characters", MaxRepoNameLen)
82	}
83	for _, r := range s {
84		switch {
85		case r >= 'A' && r <= 'Z':
86			return "uppercase letters are not allowed"
87		case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '.', r == '_', r == '-':
88		default:
89			return fmt.Sprintf("character %q is not allowed (use a-z, 0-9, '.', '_', '-')", r)
90		}
91	}
92	switch {
93	case strings.HasPrefix(s, ".") || strings.HasSuffix(s, "."):
94		return "name must not start or end with '.'"
95	case strings.Contains(s, ".."):
96		return "name must not contain '..'"
97	case strings.HasSuffix(s, gitSuffix):
98		return "name must not end in '.git'"
99	case reservedNames[s]:
100		return "name is reserved"
101	}
102	return ""
103}
104
105// OwnerMatches reports whether name is the owner of the forge, as GitHub
106// matches the owner of a repository: without regard to case. It is the one
107// rule for the owner segment of the Git URLs, of the REST subset and of the
108// references of actions. configured is ERMITE_OWNER, which is lower case (it
109// follows the rules of repository names); an empty one matches nothing.
110func OwnerMatches(configured, name string) bool {
111	return configured != "" && strings.EqualFold(configured, name)
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 3

Tags 43

↑↓ moveenter openesc close