1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6 "fmt"
7 "slices"
8 "strings"
9 "time"
10)
11
12// Visibility is the access level of a repository.
13type Visibility string
14
15// Visibility values.
16const (
17 // VisibilityPrivate hides the repository from anonymous callers.
18 VisibilityPrivate Visibility = "private"
19 // VisibilityPublic lets anonymous callers read the repository.
20 VisibilityPublic Visibility = "public"
21)
22
23// ParseVisibility parses "public" or "private".
24func ParseVisibility(s string) (Visibility, error) {
25 switch v := Visibility(s); v {
26 case VisibilityPublic, VisibilityPrivate:
27 return v, nil
28 }
29 return "", fmt.Errorf("%w: %q (want public or private)", ErrInvalidVisibility, s)
30}
31
32// String returns the visibility as text.
33func (v Visibility) String() string { return string(v) }
34
35// Repo is the metadata ermite owns about a repository. Git-owned state (refs,
36// HEAD) stays in Git.
37type Repo struct {
38 Name RepoName
39 Description string
40 // Topics are the normalized topics (see NormalizeTopics); nil for none.
41 Topics []string
42 Visibility Visibility
43 CreatedAt time.Time
44 PushedAt time.Time // zero until the first push
45 ActiveAt time.Time // latest push, merge request or issue activity
46 // Settings are the merge settings; the stores always fill them in, with
47 // the defaults for a repository that never stored any.
48 Settings RepoSettings
49}
50
51// Limits of the topics of a repository.
52const (
53 // MaxTopics is the largest number of topics of a repository.
54 MaxTopics = 20
55 // MaxTopicLen is the longest topic, in characters.
56 MaxTopicLen = 50
57)
58
59// NormalizeTopics lowercases, validates, deduplicates and sorts topics. A
60// topic is 1 to MaxTopicLen characters of a-z, 0-9 and "-", starting with a
61// letter or a digit; there are at most MaxTopics of them once deduplicated.
62// No topic gives nil. A broken rule gives an error matching ErrInvalid.
63func NormalizeTopics(topics []string) ([]string, error) {
64 var out []string
65 for _, t := range topics {
66 t = strings.ToLower(t)
67 if !validTopic(t) {
68 return nil, fmt.Errorf("%w: topic %q must be 1 to %d characters of a-z, 0-9 and -, starting with a letter or a digit", ErrInvalid, t, MaxTopicLen)
69 }
70 out = append(out, t)
71 }
72 slices.Sort(out)
73 out = slices.Compact(out)
74 if len(out) > MaxTopics {
75 return nil, fmt.Errorf("%w: %d topics, at most %d", ErrInvalid, len(out), MaxTopics)
76 }
77 return out, nil
78}
79
80func validTopic(t string) bool {
81 if t == "" || len(t) > MaxTopicLen {
82 return false
83 }
84 for i := range len(t) {
85 c := t[i]
86 switch {
87 case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
88 case c == '-' && i > 0:
89 default:
90 return false
91 }
92 }
93 return true
94}
95
96// EffectiveSettings returns the repository settings, using defaults for an unset record.
97func (r Repo) EffectiveSettings() RepoSettings {
98 if r.Settings.IsZero() {
99 return DefaultRepoSettings()
100 }
101 return r.Settings
102}