detached 14b68ea

docs(domain): state the assumptions behind the branch prefix check

No symbolic refs under refs/heads besides HEAD, and case-sensitive ref storage.
commit
14b68ead1628d55bfdc5e263a745ed0bc077fe12
author
brice · · 2026-10-12 15:17 UTC
parent
e751749
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +13 -0
M internal/domain/principal.go +8 -0
@@ -117,6 +117,14 @@ func (p Principal) BranchPrefixes() []string { return slices.Clone(p.branchPrefi
117117 // it). Without branch prefixes every ref is allowed. With them, only a branch
118118 // "refs/heads/<name>" under one of the prefixes is: tags, every other ref and
119119 // a branch outside the prefixes are not.
120+//
121+// It judges the name of the ref only, and rests on two assumptions about the
122+// repositories ermite serves. First, no symbolic ref lives under refs/heads
123+// (HEAD is the only symref): ermite never creates one and receive-pack cannot,
124+// so a name under a prefix never points at a branch outside it. Second, ref
125+// names are stored case-sensitively (Linux file systems): "refs/heads/Agent/x"
126+// is not "refs/heads/agent/x". On a case-insensitive file system the two could
127+// name the same ref and the prefixes would not hold.
120128 func (p Principal) AllowsRef(ref string) bool {
121129 if len(p.branchPrefixes) == 0 {
122130 return true
@@ -117,6 +117,14 @@ func (p Principal) BranchPrefixes() []string { return slices.Clone(p.branchPrefi
117 // it). Without branch prefixes every ref is allowed. With them, only a branch117 // it). Without branch prefixes every ref is allowed. With them, only a branch
118 // "refs/heads/<name>" under one of the prefixes is: tags, every other ref and118 // "refs/heads/<name>" under one of the prefixes is: tags, every other ref and
119 // a branch outside the prefixes are not.119 // a branch outside the prefixes are not.
120+//
121+// It judges the name of the ref only, and rests on two assumptions about the
122+// repositories ermite serves. First, no symbolic ref lives under refs/heads
123+// (HEAD is the only symref): ermite never creates one and receive-pack cannot,
124+// so a name under a prefix never points at a branch outside it. Second, ref
125+// names are stored case-sensitively (Linux file systems): "refs/heads/Agent/x"
126+// is not "refs/heads/agent/x". On a case-insensitive file system the two could
127+// name the same ref and the prefixes would not hold.
120 func (p Principal) AllowsRef(ref string) bool {128 func (p Principal) AllowsRef(ref string) bool {
121 if len(p.branchPrefixes) == 0 {129 if len(p.branchPrefixes) == 0 {
122 return true130 return true
M internal/usecase/pushes.go +5 -0
@@ -104,6 +104,11 @@ func openExisting(p domain.Principal, repo domain.Repo) error {
104104 // (protected branches, limits) will refuse here too, by returning an error
105105 // that matches domain.ErrRejected.
106106 //
107+// The prefix rule is domain.Principal.AllowsRef and shares its assumptions: no
108+// symbolic ref under refs/heads besides HEAD (ermite never creates one and
109+// receive-pack cannot), and case-sensitive ref storage (Linux). The hook sees
110+// every ref of the push, so a tag or any other ref is judged like a branch.
111+//
107112 // The token of the push was valid when the request began, but a push can run
108113 // for long: it is re-validated here, so a token revoked or expired meanwhile
109114 // stops the push before Git applies it. A failure to tell refuses it too.
@@ -104,6 +104,11 @@ func openExisting(p domain.Principal, repo domain.Repo) error {
104 // (protected branches, limits) will refuse here too, by returning an error104 // (protected branches, limits) will refuse here too, by returning an error
105 // that matches domain.ErrRejected.105 // that matches domain.ErrRejected.
106 //106 //
107+// The prefix rule is domain.Principal.AllowsRef and shares its assumptions: no
108+// symbolic ref under refs/heads besides HEAD (ermite never creates one and
109+// receive-pack cannot), and case-sensitive ref storage (Linux). The hook sees
110+// every ref of the push, so a tag or any other ref is judged like a branch.
111+//
107 // The token of the push was valid when the request began, but a push can run112 // The token of the push was valid when the request began, but a push can run
108 // for long: it is re-validated here, so a token revoked or expired meanwhile113 // for long: it is re-validated here, so a token revoked or expired meanwhile
109 // stops the push before Git applies it. A failure to tell refuses it too.114 // stops the push before Git applies it. A failure to tell refuses it too.

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 2

Tags 45

↑↓ moveenter openesc close