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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

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

Serve repositories from the root of the URL space

ed03b3d1fa16ef6b55e2773aaf10e3be13282c3a · Keenan Nemetz · 4h ago · parent d37d301f

Serve repositories from the root of the URL space

A repository is now /<name> and its Git transport /<name>.git, so the URL a
person opens, the URL a clone uses, and the Go module path are the same name.
The forge's own pages moved under /-/ (admin, accounts, the directory, the
feed, sign-in, assets), a prefix no repository name can start with: names are
[a-z0-9...] and cannot begin with "-". That leaves the router with two disjoint
spaces and removes the /r/ and /git/ prefixes, which existed only to keep
repositories away from the forge's own paths.

Routes: repoRoutes lose /r/; global, auth and admin routes gain /-/; assets
move to /-/static/ because a subtree pattern like /static/ cannot coexist with
one-segment-wildcard repo routes — Go's mux reports them as conflicting
neither-more-specific patterns. The catch-all still renders the styled 404.

Transport: githttp.Route parses /<name>.git/<rest> instead of a /git/ prefix,
and forgepaths.go hands such requests to the proxy before the mux, because a
wildcard fills a whole segment and cannot match "<name>.git". LFS stays on the
mux (<name>.git/info/lfs/...), and the Go module discovery probe moves to the
same pre-router so a package path under a public repository is answered even
when its first segments name a page tab.

Names: repo.ValidateName now refuses the four root paths the forge must keep —
static, robots.txt, sitemap.xml, sitemap-pages.xml. Everything else is an
ordinary repository name, including admin, u, and activity.

Docs and the packaged URLs follow the new layout; robots.txt, the sitemaps and
the instance activity feed point at the new paths. No aliases: the old /r/ and
/git/ URLs are gone.

Verified with ./vole workflow check and ./vole workflow run verify.

server/web/templates/issues.html +16−16

@@ -7,7 +7,7 @@
7 7 <h1>Issues</h1>
8 8 <p class="page-sub">Problems and suggestions from the people who use this project.</p>
9 9 </div>
10 {{if .CanDiscuss}}<div class="page-head-actions"><a class="button" href="/r/{{.Name}}/issues/new">{{icon "plus"}}New issue</a></div>{{end}}
10 {{if .CanDiscuss}}<div class="page-head-actions"><a class="button" href="/{{.Name}}/issues/new">{{icon "plus"}}New issue</a></div>{{end}}
11 11 </div>
12 12
13 13 {{template "issue_list" .}}
@@ -23,11 +23,11 @@
23 23 {{/* One row: the state chips and the dropdowns on the left, the search field on
24 24 the right. The form wraps the chips so a search submits the state too, and
25 25 so a swap replaces the row with the values the server rendered. */}}
26 <form method="get" action="/r/{{.Name}}/issues" class="filters" role="search" hx-get="/r/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">
26 <form method="get" action="/{{.Name}}/issues" class="filters" role="search" hx-get="/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">
27 27 <div class="chips" hx-boost="true" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">
28 <a class="chip{{if eq .State "open"}} active{{end}}" href="/r/{{.Name}}/issues?state=open{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Open <span class="chip-count">{{.Open}}</span></a>
29 <a class="chip{{if eq .State "closed"}} active{{end}}" href="/r/{{.Name}}/issues?state=closed{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Closed <span class="chip-count">{{.Closed}}</span></a>
30 <a class="chip{{if eq .State "all"}} active{{end}}" href="/r/{{.Name}}/issues?state=all{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">All <span class="chip-count">{{.Total}}</span></a>
28 <a class="chip{{if eq .State "open"}} active{{end}}" href="/{{.Name}}/issues?state=open{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Open <span class="chip-count">{{.Open}}</span></a>
29 <a class="chip{{if eq .State "closed"}} active{{end}}" href="/{{.Name}}/issues?state=closed{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Closed <span class="chip-count">{{.Closed}}</span></a>
30 <a class="chip{{if eq .State "all"}} active{{end}}" href="/{{.Name}}/issues?state=all{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">All <span class="chip-count">{{.Total}}</span></a>
31 31 </div>
32 32 <input type="hidden" name="state" value="{{.State}}">
33 33 {{if .Labels}}
@@ -51,7 +51,7 @@
51 51 <div class="filters-search">
52 52 <input name="q" type="search" placeholder="Search title and body" aria-label="Search title and body" value="{{.Filter.Query}}">
53 53 <button type="submit">{{icon "search"}}Search</button>
54 {{if .Filtered}}<a class="button secondary" href="/r/{{.Name}}/issues" hx-get="/r/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">{{icon "x"}}Clear</a>{{end}}
54 {{if .Filtered}}<a class="button secondary" href="/{{.Name}}/issues" hx-get="/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">{{icon "x"}}Clear</a>{{end}}
55 55 </div>
56 56 </form>
57 57
@@ -66,7 +66,7 @@
66 66 <tr>
67 67 <td class="fit">{{template "issue_status" .State}}</td>
68 68 <td>
69 <a class="row-title" href="/r/{{$.Name}}/issues/{{.ID}}">{{.Title}}</a>
69 <a class="row-title" href="/{{$.Name}}/issues/{{.ID}}">{{.Title}}</a>
70 70 <div class="row-meta">
71 71 <span class="sha">{{num .Number}}</span>
72 72 <span>opened by {{template "userlink" (user .Author)}} {{since .Created.Unix}}</span>
@@ -108,7 +108,7 @@
108 108 <h2>{{icon "tag"}}Labels</h2>
109 109 {{if .Error}}<p class="error">{{.Error}}</p>{{end}}
110 110 {{if .CanEditLabels}}
111 <form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/labels" class="label-form" hx-post="/r/{{.Name}}/issues/{{.Issue.ID}}/labels" hx-target="#issue-labels" hx-swap="outerHTML" hx-trigger="token-change" hx-sync="this:replace">
111 <form method="post" action="/{{.Name}}/issues/{{.Issue.ID}}/labels" class="label-form" hx-post="/{{.Name}}/issues/{{.Issue.ID}}/labels" hx-target="#issue-labels" hx-swap="outerHTML" hx-trigger="token-change" hx-sync="this:replace">
112 112 <input type="hidden" name="csrf" value="{{.CSRF}}">
113 113 {{template "token_input" (tokenPicker .Issue.Labels .AllLabels "label" "Add labels…")}}
114 114 </form>
@@ -127,13 +127,13 @@
127 127 <h2>{{icon "users"}}Assignees</h2>
128 128 {{if .Error}}<p class="error">{{.Error}}</p>{{end}}
129 129 {{if .CanEditAssignees}}
130 <form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/assignees" class="label-form" hx-post="/r/{{.Name}}/issues/{{.Issue.ID}}/assignees" hx-target="#issue-assignees" hx-swap="outerHTML" hx-trigger="token-change" hx-sync="this:replace">
130 <form method="post" action="/{{.Name}}/issues/{{.Issue.ID}}/assignees" class="label-form" hx-post="/{{.Name}}/issues/{{.Issue.ID}}/assignees" hx-target="#issue-assignees" hx-swap="outerHTML" hx-trigger="token-change" hx-sync="this:replace">
131 131 <input type="hidden" name="csrf" value="{{.CSRF}}">
132 132 {{template "token_input" (tokenPicker .Issue.Assignees .Assignable "assignee" "Add assignees…")}}
133 133 </form>
134 134 {{else}}
135 135 {{if .Issue.Assignees}}
136 <div class="label-row">{{range .Issue.Assignees}}<a class="label" href="/r/{{$.Name}}/issues?assignee={{urlquery .}}">@{{.}}</a>{{end}}</div>
136 <div class="label-row">{{range .Issue.Assignees}}<a class="label" href="/{{$.Name}}/issues?assignee={{urlquery .}}">@{{.}}</a>{{end}}</div>
137 137 {{else}}
138 138 <p class="muted">No assignees.</p>
139 139 {{end}}
@@ -148,8 +148,8 @@
148 148 <span class="page-num">{{num .Number}}</span>
149 149 <h1 class="details-title-view" data-details-title-view{{if .Details.Title.Active}} hidden{{end}}>{{.Issue.Title}}</h1>
150 150 {{if .CanEdit}}
151 <a class="title-edit-toggle" href="/r/{{.Name}}/issues/{{.Issue.ID}}/edit" data-title-edit{{if .Details.Title.Active}} hidden{{end}} aria-label="Edit title" title="Edit title">{{icon "pencil"}}</a>
152 <form class="title-edit-form" data-title-edit-form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/edit" hx-post="/r/{{.Name}}/issues/{{.Issue.ID}}/edit" hx-target="#issue-head" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']"{{if not .Details.Title.Active}} hidden{{end}}>
151 <a class="title-edit-toggle" href="/{{.Name}}/issues/{{.Issue.ID}}/edit" data-title-edit{{if .Details.Title.Active}} hidden{{end}} aria-label="Edit title" title="Edit title">{{icon "pencil"}}</a>
152 <form class="title-edit-form" data-title-edit-form method="post" action="/{{.Name}}/issues/{{.Issue.ID}}/edit" hx-post="/{{.Name}}/issues/{{.Issue.ID}}/edit" hx-target="#issue-head" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']"{{if not .Details.Title.Active}} hidden{{end}}>
153 153 <input type="hidden" name="csrf" value="{{.CSRF}}">
154 154 <input type="hidden" name="field" value="title">
155 155 <input class="details-title-input" type="text" name="title" required maxlength="200" aria-label="Title" value="{{if .Details.Title.Active}}{{.Details.Title.Value}}{{else}}{{.Issue.Title}}{{end}}" data-title-edit-input{{if .Details.Title.Active}} autofocus{{end}}>
@@ -166,7 +166,7 @@
166 166 </div>
167 167 {{if .CanClose}}
168 168 <div class="page-head-actions">
169 <form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/state">
169 <form method="post" action="/{{.Name}}/issues/{{.Issue.ID}}/state">
170 170 <input type="hidden" name="csrf" value="{{.CSRF}}">
171 171 <input type="hidden" name="state" value="{{if eq .Issue.State "open"}}closed{{else}}open{{end}}">
172 172 <button class="{{if eq .Issue.State "open"}}danger{{else}}secondary{{end}}" type="submit">{{if eq .Issue.State "open"}}{{icon "circle-x"}}Close{{else}}{{icon "rotate-ccw"}}Reopen{{end}}</button>
@@ -194,7 +194,7 @@
194 194 {{if .Description}}{{.Description}}{{else}}<p class="muted">No description.</p>{{end}}
195 195 </div>
196 196 {{if .CanEdit}}
197 <form class="comment-edit-form details-edit-form" data-comment-edit-form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/edit" hx-post="/r/{{.Name}}/issues/{{.Issue.ID}}/edit" hx-target="#issue-description" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']"{{if not .Details.Description.Active}} hidden{{end}}>
197 <form class="comment-edit-form details-edit-form" data-comment-edit-form method="post" action="/{{.Name}}/issues/{{.Issue.ID}}/edit" hx-post="/{{.Name}}/issues/{{.Issue.ID}}/edit" hx-target="#issue-description" hx-swap="outerHTML" hx-disabled-elt="find button[type='submit']"{{if not .Details.Description.Active}} hidden{{end}}>
198 198 <input type="hidden" name="csrf" value="{{.CSRF}}">
199 199 <input type="hidden" name="field" value="body">
200 200 {{if .Details.Description.Error}}<p class="error">{{.Details.Description.Error}}</p>{{end}}
@@ -217,7 +217,7 @@
217 217 <h2>{{icon "git-pull-request"}}Referenced in changes</h2>
218 218 <div class="side-list">
219 219 {{range .LinkedChanges}}
220 <a class="side-change" href="/r/{{$.Name}}/changes/{{.ID}}">
220 <a class="side-change" href="/{{$.Name}}/changes/{{.ID}}">
221 221 <span class="side-subject"><span class="side-num">{{num .Number}}</span>{{.Title}}</span>
222 222 <span class="side-meta"><span class="state {{.State}}">{{.State}}</span>{{who .Author}}{{if not .Created.IsZero}}<span>{{since .Created.Unix}}</span>{{end}}</span>
223 223 </a>
@@ -236,7 +236,7 @@
236 236 <h2>{{icon "git-commit-horizontal"}}Referenced in commits</h2>
237 237 <div class="side-list">
238 238 {{range .Backlinks}}
239 <a class="side-commit" href="/r/{{$.Name}}/commit/{{.SHA}}">
239 <a class="side-commit" href="/{{$.Name}}/commit/{{.SHA}}">
240 240 <span class="side-subject">{{.Subject}}</span>
241 241 <span class="side-meta"><span class="sha">{{short .SHA}}</span>{{if .Author}}<span>{{.Author}}</span>{{end}}<span>{{since .When}}</span></span>
242 242 </a>