Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Add browser auth, contributor access, and gated CI

6c84d387e5f608360e30484a1a5b1cff41076850 · Keenan Nemetz · 16d ago · parent c36d5073

Add browser auth, contributor access, and gated CI

Browser sign-in now uses passkeys. Enrollment links are one-time, sessions are hashed in users.db, and issue/comment/approval writes use the session account. HTTPS public_url is required for WebAuthn.

Add the contributor role between read and maintain. Contributors can discuss and open change requests, and may push only refs/heads/contrib/<account-id>/. Contributor CI is held per commit until a maintainer allows the exact tip from the repository page.

Rename the write role to maintain. write remains accepted as the historical spelling, and access.Write remains as a Go alias so existing callers and ACL rows keep working.

Update the web, CLI, queue schema, and guides for the new flows, and cover the role, namespace, auth, and CI-gating paths in tests.

server/web/issues.go +24−18

@@ -106,26 +106,29 @@ func (w *Web) Issues(rw http.ResponseWriter, r *http.Request) {
106 106 open++
107 107 }
108 108 }
109 acl, _ := m.ACL()
109 110 w.render(rw, "issues", struct {
110 111 Page
111 Name string
112 Issues []index.IssueRow
113 Labels []string
114 Assignees []string
115 Filter index.Filter
116 Total int
117 Open int
118 Filtered bool
112 Name string
113 Issues []index.IssueRow
114 Labels []string
115 Assignees []string
116 Filter index.Filter
117 Total int
118 Open int
119 Filtered bool
120 CanDiscuss bool
119 121 }{
120 Page: w.base(m.Name + " · issues"),
121 Name: m.Name,
122 Issues: rows,
123 Labels: labels,
124 Assignees: assignees,
125 Filter: filter,
126 Total: total,
127 Open: open,
128 Filtered: filter.State != "" || filter.Label != "" || filter.Assignee != "" || filter.Query != "",
122 Page: w.page(r, m.Name+" · issues"),
123 Name: m.Name,
124 Issues: rows,
125 Labels: labels,
126 Assignees: assignees,
127 Filter: filter,
128 Total: total,
129 Open: open,
130 Filtered: filter.State != "" || filter.Label != "" || filter.Assignee != "" || filter.Query != "",
131 CanDiscuss: acl != nil && w.canDiscuss(r, acl),
129 132 }, 0)
130 133 }
131 134
@@ -154,6 +157,7 @@ func (w *Web) IssueView(rw http.ResponseWriter, r *http.Request) {
154 157 for _, c := range iss.Comments {
155 158 comments = append(comments, rendered{Comment: c, Body: w.renderMarkdown(m.Name, c.Body)})
156 159 }
160 acl, _ := m.ACL()
157 161 w.render(rw, "issue", struct {
158 162 Page
159 163 Name string
@@ -162,14 +166,16 @@ func (w *Web) IssueView(rw http.ResponseWriter, r *http.Request) {
162 166 Comments []rendered
163 167 Labels string
164 168 Backlinks []git.Commit
169 CanDiscuss bool
165 170 }{
166 Page: w.base(m.Name + " · " + iss.Title),
171 Page: w.page(r, m.Name+" · "+iss.Title),
167 172 Name: m.Name,
168 173 Issue: iss,
169 174 Description: w.renderMarkdown(m.Name, iss.Description),
170 175 Comments: comments,
171 176 Labels: strings.Join(iss.Labels, ", "),
172 177 Backlinks: w.backlinks(m, iss.ID),
178 CanDiscuss: acl != nil && w.canDiscuss(r, acl),
173 179 }, 0)
174 180 }
175 181