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/admin_users.html +9−9

@@ -1,11 +1,11 @@
1 1 {{define "accounts_body"}}
2 2 {{if .Error}}<div class="flash bad">{{.Error}}</div>{{end}}
3 3
4 <p class="muted">To change a role, inspect keys or sessions, or retire an account, open it from the <a href="/u">users directory</a>.</p>
4 <p class="muted">To change a role, inspect keys or sessions, or retire an account, open it from the <a href="/-/u">users directory</a>.</p>
5 5
6 6 <h2>Invite a person</h2>
7 7 <p class="muted">Send them a one-time link: they choose a passkey, then can add an SSH key.</p>
8 <form method="post" action="/users" class="form-block">
8 <form method="post" action="/-/users" class="form-block">
9 9 <input type="hidden" name="csrf" value="{{.CSRF}}">
10 10 <div class="card form-card">
11 11 <label>Account name
@@ -18,7 +18,7 @@
18 18
19 19 <h2>Create a bot account</h2>
20 20 <p class="muted">For a runner or another automation. It acts with the SSH key you paste here and cannot sign in to the browser or become an instance admin.</p>
21 <form method="post" action="/users/bots" class="form-block">
21 <form method="post" action="/-/users/bots" class="form-block">
22 22 <input type="hidden" name="csrf" value="{{.CSRF}}">
23 23 <div class="card form-card">
24 24 <label>Account name
@@ -45,7 +45,7 @@
45 45 </div>
46 46 {{if not .Account.Bot}}
47 47 <div class="page-head-actions">
48 <form class="inline" method="post" action="/users/{{.Account.Name}}/invite">
48 <form class="inline" method="post" action="/-/users/{{.Account.Name}}/invite">
49 49 <input type="hidden" name="csrf" value="{{.CSRF}}">
50 50 <button type="submit" class="secondary">{{icon "external-link"}}New enrollment link</button>
51 51 </form>
@@ -57,7 +57,7 @@
57 57
58 58 <h2>Instance role</h2>
59 59 {{$role := .Account.Role.String}}
60 <form method="post" action="/users/{{.Account.Name}}/role" class="form-block">
60 <form method="post" action="/-/users/{{.Account.Name}}/role" class="form-block">
61 61 <input type="hidden" name="csrf" value="{{.CSRF}}">
62 62 <div class="card form-card">
63 63 {{template "role-picker" (rolePicker "role" $role (printf "Role for %s" .Account.Name) .RoleHelp .RoleScope false)}}
@@ -79,7 +79,7 @@
79 79 <td>{{.Type}}</td>
80 80 <td class="muted">{{.Comment}}</td>
81 81 <td>
82 <form class="inline" method="post" action="/users/{{$.Account.Name}}/keys/remove">
82 <form class="inline" method="post" action="/-/users/{{$.Account.Name}}/keys/remove">
83 83 <input type="hidden" name="csrf" value="{{$.CSRF}}">
84 84 <input type="hidden" name="fingerprint" value="{{.Fingerprint}}">
85 85 <button type="submit" class="danger">Remove</button>
@@ -99,7 +99,7 @@
99 99 <div class="with-more">
100 100 <h2>Sessions</h2>
101 101 {{if .Sessions}}
102 <form class="inline" method="post" action="/users/{{.Account.Name}}/sessions/revoke">
102 <form class="inline" method="post" action="/-/users/{{.Account.Name}}/sessions/revoke">
103 103 <input type="hidden" name="csrf" value="{{.CSRF}}">
104 104 <button type="submit" class="link">Revoke all sessions</button>
105 105 </form>
@@ -126,7 +126,7 @@
126 126 {{end}}
127 127
128 128 <h2>Danger zone</h2>
129 <form method="post" action="/users/{{.Account.Name}}/retire" class="form-block">
129 <form method="post" action="/-/users/{{.Account.Name}}/retire" class="form-block">
130 130 <input type="hidden" name="csrf" value="{{.CSRF}}">
131 131 <div class="card form-card danger-zone">
132 132 <h3>Retire account</h3>
@@ -160,7 +160,7 @@
160 160 </div>
161 161
162 162 <div class="form-actions">
163 <a class="button secondary" href="/users/{{.Account.Name}}">{{icon "user"}}Back to account</a>
163 <a class="button secondary" href="/-/users/{{.Account.Name}}">{{icon "user"}}Back to account</a>
164 164 </div>
165 165 {{template "footer" .}}
166 166 {{end}}