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/chrome.html +30−30

@@ -12,30 +12,30 @@
12 12 {{if .NoIndex}}<meta name="robots" content="noindex">{{end}}
13 13 <meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
14 14 <title>{{.Title}} · {{.InstanceName}}</title>
15 <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32.png" media="(prefers-color-scheme: light)">
16 <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32-dark.png" media="(prefers-color-scheme: dark)">
17 <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16.png" media="(prefers-color-scheme: light)">
18 <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16-dark.png" media="(prefers-color-scheme: dark)">
19 <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" sizes="any">
15 <link rel="icon" type="image/png" sizes="32x32" href="/-/static/favicon-32.png" media="(prefers-color-scheme: light)">
16 <link rel="icon" type="image/png" sizes="32x32" href="/-/static/favicon-32-dark.png" media="(prefers-color-scheme: dark)">
17 <link rel="icon" type="image/png" sizes="16x16" href="/-/static/favicon-16.png" media="(prefers-color-scheme: light)">
18 <link rel="icon" type="image/png" sizes="16x16" href="/-/static/favicon-16-dark.png" media="(prefers-color-scheme: dark)">
19 <link rel="icon" type="image/svg+xml" href="/-/static/favicon.svg" sizes="any">
20 20 {{if .InstanceFavicon}}<link rel="icon" type="image/svg+xml" href="{{.InstanceFavicon}}" sizes="any">{{end}}
21 <script src="/static/theme.js"></script>
22 <link rel="stylesheet" href="/static/vole.css">
23 <link rel="stylesheet" href="/static/everforest.css">
24 <link rel="stylesheet" href="/static/palettes.css">
21 <script src="/-/static/theme.js"></script>
22 <link rel="stylesheet" href="/-/static/vole.css">
23 <link rel="stylesheet" href="/-/static/everforest.css">
24 <link rel="stylesheet" href="/-/static/palettes.css">
25 25 {{range .InstanceThemes}}<link rel="stylesheet" href="{{.URL}}">
26 26 {{end}}{{if .InstanceCSS}}<link rel="stylesheet" href="{{.InstanceCSS}}">{{end}}
27 <script src="/static/htmx.min.js" defer></script>
28 <script src="/static/auth.js" defer></script>
29 <script src="/static/app.js" defer></script>
27 <script src="/-/static/htmx.min.js" defer></script>
28 <script src="/-/static/auth.js" defer></script>
29 <script src="/-/static/app.js" defer></script>
30 30 </head>
31 31 <body>
32 32 <header class="topbar">
33 33 <a class="brand" href="/" aria-label="{{.InstanceName}} home"><span class="mark" aria-hidden="true"></span></a>
34 34 <nav class="topnav" aria-label="Global">
35 35 <a class="{{if eq .Section "home"}}active{{end}}" href="/"{{if eq .Section "home"}} aria-current="page"{{end}}>{{icon "house"}}Home</a>
36 <a class="{{if eq .Section "activity"}}active{{end}}" href="/activity"{{if eq .Section "activity"}} aria-current="page"{{end}}>{{icon "activity"}}Activity</a>
37 <a class="{{if eq .Section "people"}}active{{end}}" href="/u"{{if eq .Section "people"}} aria-current="page"{{end}}>{{icon "users"}}Users</a>
38 {{if .InstanceAdmin}}<a class="{{if eq .Section "admin"}}active{{end}}" href="/admin"{{if eq .Section "admin"}} aria-current="page"{{end}}>{{icon "sliders-horizontal"}}Admin</a>{{end}}
36 <a class="{{if eq .Section "activity"}}active{{end}}" href="/-/activity"{{if eq .Section "activity"}} aria-current="page"{{end}}>{{icon "activity"}}Activity</a>
37 <a class="{{if eq .Section "people"}}active{{end}}" href="/-/u"{{if eq .Section "people"}} aria-current="page"{{end}}>{{icon "users"}}Users</a>
38 {{if .InstanceAdmin}}<a class="{{if eq .Section "admin"}}active{{end}}" href="/-/admin"{{if eq .Section "admin"}} aria-current="page"{{end}}>{{icon "sliders-horizontal"}}Admin</a>{{end}}
39 39 </nav>
40 40 <div class="topsearch" role="search">
41 41 <button type="button" class="topsearch-box" data-palette-open aria-haspopup="dialog" aria-controls="search-palette" aria-keyshortcuts="Control+K Meta+K">
@@ -53,14 +53,14 @@
53 53 <span class="account-caret">{{icon "chevron-right"}}</span>
54 54 </summary>
55 55 <div class="account-pop">
56 <a href="/account">{{icon "user"}}Account</a>
56 <a href="/-/account">{{icon "user"}}Account</a>
57 57 <div class="account-sep"></div>
58 <form method="post" action="/logout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button type="submit">{{icon "log-out"}}Sign out</button></form>
58 <form method="post" action="/-/logout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button type="submit">{{icon "log-out"}}Sign out</button></form>
59 59 </div>
60 60 </details>
61 61 {{else}}
62 {{if .DevLogin}}<a href="/dev/login">{{icon "user"}}Dev sign-in</a>{{end}}
63 <a href="/login">{{icon "user"}}Sign in</a>
62 {{if .DevLogin}}<a href="/-/dev/login">{{icon "user"}}Dev sign-in</a>{{end}}
63 <a href="/-/login">{{icon "user"}}Sign in</a>
64 64 {{end}}
65 65 </div>
66 66 </header>
@@ -69,7 +69,7 @@
69 69 global bar opens it; the dialog itself never navigates to a page. */}}
70 70 <dialog class="palette" id="search-palette" aria-label="Search issues, changes and comments">
71 71 <form class="palette-search" role="search"
72 hx-get="/search/palette" hx-trigger="input changed delay:250ms, search"
72 hx-get="/-/search/palette" hx-trigger="input changed delay:250ms, search"
73 73 hx-target="#palette-results" hx-swap="innerHTML" hx-sync="this:replace">
74 74 <span aria-hidden="true">{{icon "search"}}</span>
75 75 <input type="search" name="q" id="search-palette-input"
@@ -141,19 +141,19 @@
141 141 state. It pauses while the tab is hidden, so a forgotten page does not
142 142 keep asking. */}}
143 143 <nav class="tabs" aria-label="Repository"
144 hx-get="/r/{{.Name}}/counts"
144 hx-get="/{{.Name}}/counts"
145 145 hx-trigger="every 30s [document.visibilityState === 'visible']"
146 146 hx-sync="this:replace"
147 147 hx-swap="none">
148 <a class="repotab{{if eq .Tab "overview"}} active{{end}}" href="/r/{{.Name}}"{{if eq .Tab "overview"}} aria-current="page"{{end}}>{{icon "layout-dashboard"}}Overview</a>
149 <a href="/r/{{.Name}}/activity"{{if eq .Tab "activity"}} class="active" aria-current="page"{{end}}>{{icon "activity"}}Activity</a>
150 <a href="/r/{{.Name}}/code"{{if eq .Tab "code"}} class="active" aria-current="page"{{end}}>{{icon "code"}}Code</a>
151 <a href="/r/{{.Name}}/issues"{{if eq .Tab "issues"}} class="active" aria-current="page"{{end}}>{{icon "circle-dot"}}Issues {{template "repo_issue_count" .}}</a>
152 <a href="/r/{{.Name}}/changes"{{if eq .Tab "changes"}} class="active" aria-current="page"{{end}}>{{icon "git-pull-request"}}Changes {{template "repo_request_count" .}}</a>
153 <a href="/r/{{.Name}}/releases"{{if eq .Tab "releases"}} class="active" aria-current="page"{{end}}>{{icon "tag"}}Releases</a>
154 <a href="/r/{{.Name}}/runs"{{if eq .Tab "runs"}} class="active" aria-current="page"{{end}}>{{icon "play"}}Runs</a>
155 <a href="/r/{{.Name}}/locks"{{if eq .Tab "locks"}} class="active" aria-current="page"{{end}}>{{icon "lock"}}Locks</a>
156 {{if .CanAdmin}}<a href="/r/{{.Name}}/settings"{{if eq .Tab "settings"}} class="active" aria-current="page"{{end}}>{{icon "sliders-horizontal"}}Settings</a>{{end}}
148 <a class="repotab{{if eq .Tab "overview"}} active{{end}}" href="/{{.Name}}"{{if eq .Tab "overview"}} aria-current="page"{{end}}>{{icon "layout-dashboard"}}Overview</a>
149 <a href="/{{.Name}}/activity"{{if eq .Tab "activity"}} class="active" aria-current="page"{{end}}>{{icon "activity"}}Activity</a>
150 <a href="/{{.Name}}/code"{{if eq .Tab "code"}} class="active" aria-current="page"{{end}}>{{icon "code"}}Code</a>
151 <a href="/{{.Name}}/issues"{{if eq .Tab "issues"}} class="active" aria-current="page"{{end}}>{{icon "circle-dot"}}Issues {{template "repo_issue_count" .}}</a>
152 <a href="/{{.Name}}/changes"{{if eq .Tab "changes"}} class="active" aria-current="page"{{end}}>{{icon "git-pull-request"}}Changes {{template "repo_request_count" .}}</a>
153 <a href="/{{.Name}}/releases"{{if eq .Tab "releases"}} class="active" aria-current="page"{{end}}>{{icon "tag"}}Releases</a>
154 <a href="/{{.Name}}/runs"{{if eq .Tab "runs"}} class="active" aria-current="page"{{end}}>{{icon "play"}}Runs</a>
155 <a href="/{{.Name}}/locks"{{if eq .Tab "locks"}} class="active" aria-current="page"{{end}}>{{icon "lock"}}Locks</a>
156 {{if .CanAdmin}}<a href="/{{.Name}}/settings"{{if eq .Tab "settings"}} class="active" aria-current="page"{{end}}>{{icon "sliders-horizontal"}}Settings</a>{{end}}
157 157 </nav>
158 158 </div>
159 159 {{end}}