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 | ||