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