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/markdown_test.go +19−19

@@ -26,55 +26,55 @@ func TestDocHrefResolvesAgainstTheDocument(t *testing.T) {
26 26 }{
27 27 {
28 28 name: "markdown opens rendered", doc: root, dest: "docs/guide.md",
29 want: "/r/demo/blob/main/docs/guide.md?preview=1", ok: true,
29 want: "/demo/blob/main/docs/guide.md?preview=1", ok: true,
30 30 },
31 31 {
32 32 name: "fragment follows the preview flag", doc: root, dest: "docs/guide.md#using-it",
33 want: "/r/demo/blob/main/docs/guide.md?preview=1#using-it", ok: true,
33 want: "/demo/blob/main/docs/guide.md?preview=1#using-it", ok: true,
34 34 },
35 35 {
36 36 name: "other files open as source", doc: root, dest: "LICENSE",
37 want: "/r/demo/blob/main/LICENSE", ok: true,
37 want: "/demo/blob/main/LICENSE", ok: true,
38 38 },
39 39 {
40 40 name: "nested non-markdown", doc: root, dest: ".vole/workflows/verify.yaml",
41 want: "/r/demo/blob/main/.vole/workflows/verify.yaml", ok: true,
41 want: "/demo/blob/main/.vole/workflows/verify.yaml", ok: true,
42 42 },
43 43 {
44 44 name: "a directory opens the tree", doc: root, dest: "docs/",
45 want: "/r/demo/tree/main/docs", ok: true,
45 want: "/demo/tree/main/docs", ok: true,
46 46 },
47 47 {
48 48 name: "the document's own directory", doc: guide, dest: "./",
49 want: "/r/demo/tree/main/docs", ok: true,
49 want: "/demo/tree/main/docs", ok: true,
50 50 },
51 51 {
52 52 name: "the repository root", doc: root, dest: "./",
53 want: "/r/demo/code?ref=main", ok: true,
53 want: "/demo/code?ref=main", ok: true,
54 54 },
55 55 {
56 56 name: "parent climbs one level", doc: guide, dest: "../LICENSE",
57 want: "/r/demo/blob/main/LICENSE", ok: true,
57 want: "/demo/blob/main/LICENSE", ok: true,
58 58 },
59 59 {
60 60 name: "climbing stops at the root", doc: guide, dest: "../../../LICENSE",
61 want: "/r/demo/blob/main/LICENSE", ok: true,
61 want: "/demo/blob/main/LICENSE", ok: true,
62 62 },
63 63 {
64 64 name: "a sibling from a subdirectory", doc: guide, dest: "sibling.md",
65 want: "/r/demo/blob/main/docs/sibling.md?preview=1", ok: true,
65 want: "/demo/blob/main/docs/sibling.md?preview=1", ok: true,
66 66 },
67 67 {
68 68 name: "images point at raw", doc: root, dest: "img/shot.png", image: true,
69 want: "/r/demo/raw/main/img/shot.png", ok: true,
69 want: "/demo/raw/main/img/shot.png", ok: true,
70 70 },
71 71 {
72 72 name: "images resolve against their own file", doc: guide, dest: "shot.png", image: true,
73 want: "/r/demo/raw/main/docs/shot.png", ok: true,
73 want: "/demo/raw/main/docs/shot.png", ok: true,
74 74 },
75 75 {
76 76 name: "a ref with a slash stays one segment", doc: feature, dest: "LICENSE",
77 want: "/r/demo/blob/feat%2Ftree/LICENSE", ok: true,
77 want: "/demo/blob/feat%2Ftree/LICENSE", ok: true,
78 78 },
79 79
80 80 // Everything below already names somewhere, so the document does not get
@@ -82,7 +82,7 @@ func TestDocHrefResolvesAgainstTheDocument(t *testing.T) {
82 82 {name: "scheme", doc: root, dest: "https://example.com/x"},
83 83 {name: "mailto", doc: root, dest: "mailto:someone@example.com"},
84 84 {name: "protocol relative", doc: root, dest: "//example.com/x"},
85 {name: "site absolute", doc: root, dest: "/r/demo/issues/01M5JR2JK29KKMAA1T9MDNTPE1"},
85 {name: "site absolute", doc: root, dest: "/demo/issues/01M5JR2JK29KKMAA1T9MDNTPE1"},
86 86 {name: "same document anchor", doc: root, dest: "#why-vole"},
87 87 {name: "same document query", doc: root, dest: "?preview=1"},
88 88 {name: "empty", doc: root, dest: ""},
@@ -116,17 +116,17 @@ func TestCrossReferencesLinkBothForms(t *testing.T) {
116 116 {
117 117 name: "ulid",
118 118 in: "fixed in #" + id + "\n",
119 want: "fixed in [#" + id + "](/r/demo/issues/" + id + ")\n",
119 want: "fixed in [#" + id + "](/demo/issues/" + id + ")\n",
120 120 },
121 121 {
122 122 name: "number",
123 123 in: "see #42 for the reason\n",
124 want: "see [#42](/r/demo/n/42) for the reason\n",
124 want: "see [#42](/demo/n/42) for the reason\n",
125 125 },
126 126 {
127 127 name: "number at end of line",
128 128 in: "closes #7",
129 want: "closes [#7](/r/demo/n/7)",
129 want: "closes [#7](/demo/n/7)",
130 130 },
131 131 {
132 132 name: "inside a fence is literal",
@@ -149,7 +149,7 @@ func TestCrossReferencesLinkBothForms(t *testing.T) {
149 149 }
150 150
151 151 // The instance introduction has no repository, so a `#42` in it must stay the
152 // text it was written as. Linking it would produce /r//n/42, which is a page
152 // text it was written as. Linking it would produce //n/42, which is a page
153 153 // that cannot exist, and it is the kind of broken link only a reader notices.
154 154 func TestInstanceIntroIsNotLinkedIntoARepository(t *testing.T) {
155 155 t.Parallel()
@@ -160,7 +160,7 @@ func TestInstanceIntroIsNotLinkedIntoARepository(t *testing.T) {
160 160 t.Fatalf("intro is missing %q:\n%s", want, got)
161 161 }
162 162 }
163 for _, unwanted := range []string{"<script", "/r//"} {
163 for _, unwanted := range []string{"<script", "//"} {
164 164 if strings.Contains(got, unwanted) {
165 165 t.Fatalf("intro contains %q:\n%s", unwanted, got)
166 166 }