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