Commit 936ce1198d
936ce1198d634a620526e3c460f36e8b4661cefa
parent: b924e88c83
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-20 00:07 UTC
policy: a repository name may start with a dot
Owner names still may not: an owner is a top-level route.
Ref #236
internal/policy/names.go
+14 −4
| @@ -34,15 +34,23 @@ var reservedNames = map[string]bool{ |
| 34 | 34 | } |
| 35 | 35 | |
| 36 | 36 | // namePat matches valid user, org, and repo names: lowercase alphanumerics, |
| 37 | | // dot, dash, underscore; must start with an alphanumeric. Dots are further |
| 38 | | // restricted by ValidateName to avoid "." / ".." and ".git" suffixes. |
| 39 | | var namePat = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,62}$`) |
| 37 | // dot, dash, underscore; must start with an alphanumeric, or with a single |
| 38 | // dot before one. A leading dot marks a repository as infrastructure rather |
| 39 | // than a project — .gitbay holds an owner's profile content — and is refused |
| 40 | // for owners by ValidateOwnerName. Dots are further restricted by |
| 41 | // ValidateName to avoid "." / ".." and ".git" suffixes. |
| 42 | var namePat = regexp.MustCompile(`^\.?[a-z0-9][a-z0-9._-]{0,61}$`) |
| 40 | 43 | |
| 41 | 44 | // ValidateOwnerName checks a username or org name. |
| 42 | 45 | func ValidateOwnerName(name string) error { |
| 43 | 46 | if err := ValidateName(name); err != nil { |
| 44 | 47 | return err |
| 45 | 48 | } |
| 49 | // The leading dot is a repository affordance. An owner is a top-level |
| 50 | // route, and /.gitbay is not one. |
| 51 | if strings.HasPrefix(name, ".") { |
| 52 | return fmt.Errorf("invalid name %q: must start with a letter or digit", name) |
| 53 | } |
| 46 | 54 | if reservedNames[name] { |
| 47 | 55 | return fmt.Errorf("name %q is reserved", name) |
| 48 | 56 | } |
| @@ -58,7 +66,9 @@ func ValidateName(name string) error { |
| 58 | 66 | if name == "." || name == ".." { |
| 59 | 67 | return fmt.Errorf("invalid name %q", name) |
| 60 | 68 | } |
| 61 | | if len(name) > 4 && name[len(name)-4:] == ".git" { |
| 69 | // HasSuffix covers "repo.git" and the bare ".git" the leading-dot rule |
| 70 | // would otherwise let through. |
| 71 | if strings.HasSuffix(name, ".git") { |
| 62 | 72 | return fmt.Errorf("invalid name %q: must not end in .git", name) |
| 63 | 73 | } |
| 64 | 74 | // /{owner}/activity.atom is the owner's feed; a repository by that |
internal/policy/names_test.go
+35 −1
| @@ -1,6 +1,9 @@ |
| 1 | 1 | package policy |
| 2 | 2 | |
| 3 | | import "testing" |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | ) |
| 4 | 7 | |
| 5 | 8 | func TestValidateOwnerName(t *testing.T) { |
| 6 | 9 | valid := []string{"alice", "krz", "a", "user-1", "a.b_c", "0day"} |
| @@ -43,3 +46,34 @@ func TestRepoNameAllowsReservedWords(t *testing.T) { |
| 43 | 46 | t.Error("ValidateName(\"activity.atom\") = nil, want error") |
| 44 | 47 | } |
| 45 | 48 | } |
| 49 | |
| 50 | func TestRepoNameAllowsLeadingDot(t *testing.T) { |
| 51 | // .gitbay holds an owner's profile content; a leading dot marks a |
| 52 | // repository as infrastructure rather than a project. |
| 53 | for _, n := range []string{".gitbay", ".dotfiles", ".a"} { |
| 54 | if err := ValidateName(n); err != nil { |
| 55 | t.Errorf("ValidateName(%q) = %v, want nil", n, err) |
| 56 | } |
| 57 | } |
| 58 | for _, n := range []string{".", "..", ".git", "repo.git", "..a", ".-a"} { |
| 59 | if err := ValidateName(n); err == nil { |
| 60 | t.Errorf("ValidateName(%q) = nil, want error", n) |
| 61 | } |
| 62 | } |
| 63 | // The ceiling is 63 characters, the dot included. |
| 64 | if err := ValidateName("." + strings.Repeat("a", 62)); err != nil { |
| 65 | t.Errorf("63-character dotted name rejected: %v", err) |
| 66 | } |
| 67 | if err := ValidateName("." + strings.Repeat("a", 63)); err == nil { |
| 68 | t.Error("64-character dotted name accepted") |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | func TestOwnerNameRefusesLeadingDot(t *testing.T) { |
| 73 | // The dot is a repository affordance. An owner is a top-level route. |
| 74 | for _, n := range []string{".gitbay", ".hidden", ".a"} { |
| 75 | if err := ValidateOwnerName(n); err == nil { |
| 76 | t.Errorf("ValidateOwnerName(%q) = nil, want error", n) |
| 77 | } |
| 78 | } |
| 79 | } |