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 · 3h 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/code.html +19−19

@@ -8,8 +8,8 @@
8 8 <p class="page-sub">{{.Count}} commit(s) on <code>{{.Ref}}</code></p>
9 9 </div>
10 10 <div class="page-head-actions">
11 <a class="button secondary" href="/r/{{.Name}}/compare">{{icon "git-branch"}}Compare</a>
12 <a class="button secondary" href="/r/{{.Name}}/commits/{{esc .Ref}}">{{icon "history"}}History</a>
11 <a class="button secondary" href="/{{.Name}}/compare">{{icon "git-branch"}}Compare</a>
12 <a class="button secondary" href="/{{.Name}}/commits/{{esc .Ref}}">{{icon "history"}}History</a>
13 13 </div>
14 14 </div>
15 15
@@ -18,7 +18,7 @@
18 18 right. */}}
19 19 <div class="filters">
20 20 {{if .Branches}}
21 <form method="get" action="/r/{{.Name}}/code" class="inline">
21 <form method="get" action="/{{.Name}}/code" class="inline">
22 22 <label class="filter-field">
23 23 <span class="filter-label">Branch</span>
24 24 <select name="ref" data-autosubmit>
@@ -29,7 +29,7 @@
29 29 </form>
30 30 {{end}}
31 31 {{if .Tags}}
32 <form method="get" action="/r/{{.Name}}/code" class="inline">
32 <form method="get" action="/{{.Name}}/code" class="inline">
33 33 <label class="filter-field">
34 34 <span class="filter-label">Tag</span>
35 35 <select name="ref" data-autosubmit>
@@ -40,7 +40,7 @@
40 40 </form>
41 41 {{end}}
42 42 <div class="filters-search">
43 <form method="get" action="/r/{{.Name}}/search" class="inline" role="search">
43 <form method="get" action="/{{.Name}}/search" class="inline" role="search">
44 44 <input type="hidden" name="ref" value="{{.Ref}}">
45 45 <input type="search" name="q" placeholder="Search code" aria-label="Search code">
46 46 <button type="submit">{{icon "search"}}Search</button>
@@ -50,9 +50,9 @@
50 50
51 51 {{range .Latest}}
52 52 <div class="latest">
53 <a href="/r/{{$.Name}}/commit/{{.SHA}}">{{.Subject}}</a>
53 <a href="/{{$.Name}}/commit/{{.SHA}}">{{.Subject}}</a>
54 54 <span class="muted">{{.Author}} · {{since .When}}</span>
55 <a class="sha" href="/r/{{$.Name}}/commit/{{.SHA}}">{{short .SHA}}</a>
55 <a class="sha" href="/{{$.Name}}/commit/{{.SHA}}">{{short .SHA}}</a>
56 56 </div>
57 57 {{end}}
58 58
@@ -93,22 +93,22 @@
93 93 <div class="code-tabbar">
94 94 <span class="code-tab-icon">{{if or (eq .Mode "tree") (and (eq .Mode "overview") (not .Readme))}}{{icon "folder"}}{{else if eq .Mode "blob"}}{{icon "file"}}{{else}}{{icon "book-open"}}{{end}}</span>
95 95 <nav class="code-crumbs" aria-label="Breadcrumb">
96 <a href="/r/{{.Name}}/code?ref={{esc .Ref}}" hx-get="/r/{{.Name}}/code?ref={{esc .Ref}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{.Ref}}</a>
97 {{range $i, $c := .Crumbs}}<span class="sep">/</span>{{if eq (inc $i) (len $.Crumbs)}}<span class="current" title="{{$c.Path}}">{{$c.Name}}</span>{{else}}<a href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl $c.Path}}" hx-get="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl $c.Path}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{$c.Name}}</a>{{end}}{{end}}
96 <a href="/{{.Name}}/code?ref={{esc .Ref}}" hx-get="/{{.Name}}/code?ref={{esc .Ref}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{.Ref}}</a>
97 {{range $i, $c := .Crumbs}}<span class="sep">/</span>{{if eq (inc $i) (len $.Crumbs)}}<span class="current" title="{{$c.Path}}">{{$c.Name}}</span>{{else}}<a href="/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl $c.Path}}" hx-get="/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl $c.Path}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{$c.Name}}</a>{{end}}{{end}}
98 98 {{if not .Crumbs}}<span class="sep">/</span><span class="current">Files</span>{{end}}
99 99 </nav>
100 100 <span class="code-tab-actions">
101 101 {{if eq .Mode "blob"}}
102 102 {{if and .IsMarkdown (not .Truncated)}}
103 <a class="{{if not .Preview}}active{{end}}" href="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}" hx-get="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">source</a>
104 <a class="{{if .Preview}}active{{end}}" href="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}?preview=1" hx-get="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}?preview=1" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">preview</a>
103 <a class="{{if not .Preview}}active{{end}}" href="/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}" hx-get="/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">source</a>
104 <a class="{{if .Preview}}active{{end}}" href="/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}?preview=1" hx-get="/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .Path}}?preview=1" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">preview</a>
105 105 {{end}}
106 <a href="/r/{{.Name}}/commits/{{esc .Ref}}/{{pathurl .Path}}">history</a>
107 <a href="/r/{{.Name}}/blame/{{esc .Ref}}/{{pathurl .Path}}">blame</a>
108 <a href="/r/{{.Name}}/raw/{{esc .Ref}}/{{pathurl .Path}}">raw</a>
106 <a href="/{{.Name}}/commits/{{esc .Ref}}/{{pathurl .Path}}">history</a>
107 <a href="/{{.Name}}/blame/{{esc .Ref}}/{{pathurl .Path}}">blame</a>
108 <a href="/{{.Name}}/raw/{{esc .Ref}}/{{pathurl .Path}}">raw</a>
109 109 <span class="muted">{{bytes .Size}}</span>
110 110 {{else if and (eq .Mode "overview") .Readme}}
111 <a href="/r/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .ReadmeName}}">source</a>
111 <a href="/{{.Name}}/blob/{{esc .Ref}}/{{pathurl .ReadmeName}}">source</a>
112 112 {{else if .Entries}}
113 113 <span class="muted">{{len .Entries}} item(s)</span>
114 114 {{end}}
@@ -140,15 +140,15 @@ git push -u origin {{.Ref}}</pre>
140 140 <table>
141 141 <tbody>
142 142 {{if .Path}}
143 <tr><td><a class="dir parent" href="{{if .Parent}}/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl .Parent}}{{else}}/r/{{$.Name}}/tree/{{esc $.Ref}}{{end}}" hx-get="{{if .Parent}}/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl .Parent}}{{else}}/r/{{$.Name}}/tree/{{esc $.Ref}}{{end}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">..</a></td></tr>
143 <tr><td><a class="dir parent" href="{{if .Parent}}/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl .Parent}}{{else}}/{{$.Name}}/tree/{{esc $.Ref}}{{end}}" hx-get="{{if .Parent}}/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl .Parent}}{{else}}/{{$.Name}}/tree/{{esc $.Ref}}{{end}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">..</a></td></tr>
144 144 {{end}}
145 145 {{range .Entries}}
146 146 <tr>
147 147 <td>
148 148 {{if eq .Type "tree"}}
149 <a class="dir" href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-get="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{icon "folder"}}{{.Name}}/</a>
149 <a class="dir" href="/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-get="/{{$.Name}}/tree/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{icon "folder"}}{{.Name}}/</a>
150 150 {{else}}
151 <a class="file {{fileclass .Name}}" href="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-get="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{icon (fileicon .Name)}}{{.Name}}</a>
151 <a class="file {{fileclass .Name}}" href="/{{$.Name}}/blob/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-get="/{{$.Name}}/blob/{{esc $.Ref}}/{{pathurl (pathjoin $.Path .Name)}}" hx-target="#code-main" hx-swap="outerHTML" hx-push-url="true">{{icon (fileicon .Name)}}{{.Name}}</a>
152 152 {{end}}
153 153 </td>
154 154 </tr>
@@ -166,7 +166,7 @@ git push -u origin {{.Ref}}</pre>
166 166 {{if .Binary}}
167 167 <div class="card">
168 168 <p class="muted">Binary file · {{bytes .Size}}. It is not shown inline.</p>
169 <p><a class="button secondary" href="/r/{{.Name}}/raw/{{esc .Ref}}/{{pathurl .Path}}">Download</a></p>
169 <p><a class="button secondary" href="/{{.Name}}/raw/{{esc .Ref}}/{{pathurl .Path}}">Download</a></p>
170 170 </div>
171 171 {{else}}
172 172 {{if .Truncated}}<p class="muted">Showing the first 1 MiB.</p>{{end}}