vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Publish repositories as Go modules and move the module path to rodent.software
d37d301fd4881ee37855387377db5a7aed843017 · Keenan Nemetz · 21h ago · parent 1c363d53
Publish repositories as Go modules and move the module path to rodent.software The module named a GitHub repository that does not exist, and a module path under the forge could not be fetched anyway: the go command resolves an import path by asking it for a go-import tag first, and the forge served only pages. Answer the <host>/<repository> discovery probe from the catch-all route with the repository's tag, naming its Git transport and that prefix, so a public repository is importable with no vanity service in front of the instance. Only a repository an anonymous reader can read answers: the tag is fetched without credentials, so a tag for a private repository would confirm its existence to anyone who guessed the name. The prefix host comes from public_url, never the request's Host header, for the same reason the clone URL does. With the path resolvable, rename the module to rodent.software/vole: imports, the build line in the getting-started guide, and the packaged units and metadata follow it. docs/go-modules.md explains the lookup, what the instance must have, and why a repository named after a route cannot claim the prefix.
server/goimport_test.go +80
| @@ -0,0 +1,80 @@ | |||
| 1 | package server_test | ||
| 2 | |||
| 3 | // A repository is a Go module without a vanity service: the go command's | ||
| 4 | // discovery probe at <host>/<repository> gets a go-import tag naming the Git | ||
| 5 | // transport, and nothing in front of the forge has to serve it. | ||
| 6 | |||
| 7 | import ( | ||
| 8 | "fmt" | ||
| 9 | "net/http" | ||
| 10 | "strings" | ||
| 11 | "testing" | ||
| 12 | |||
| 13 | "rodent.software/vole/forge/config" | ||
| 14 | ) | ||
| 15 | |||
| 16 | func TestPublicRepositoryServesGoImportMetadata(t *testing.T) { | ||
| 17 | t.Parallel() | ||
| 18 | const site = "https://forge.example.com" | ||
| 19 | f := newFixtureWith(t, func(c *config.Config) { c.PublicURL = site }) | ||
| 20 | f.publish(f.repo) | ||
| 21 | |||
| 22 | tag := fmt.Sprintf(`<meta name="go-import" content="forge.example.com/%s git %s/git/%s.git">`, f.repo, site, f.repo) | ||
| 23 | for _, path := range []string{ | ||
| 24 | "/" + f.repo + "?go-get=1", | ||
| 25 | // A package inside the module asks at its own path first; the same tag | ||
| 26 | // answers for the whole prefix, which is what makes one lookup enough. | ||
| 27 | "/" + f.repo + "/server/web?go-get=1", | ||
| 28 | "/" + f.repo + "/?go-get=1", | ||
| 29 | } { | ||
| 30 | code, body := f.get(path) | ||
| 31 | if code != http.StatusOK { | ||
| 32 | t.Fatalf("%s = %d, want 200", path, code) | ||
| 33 | } | ||
| 34 | if !strings.Contains(body, tag) { | ||
| 35 | t.Fatalf("%s does not carry the go-import tag:\n%s", path, body) | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | // The tag is HTML the go command parses, and nothing else is. | ||
| 40 | resp, err := http.Get(f.http.URL + "/" + f.repo + "?go-get=1") | ||
| 41 | if err != nil { | ||
| 42 | t.Fatal(err) | ||
| 43 | } | ||
| 44 | resp.Body.Close() | ||
| 45 | if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") { | ||
| 46 | t.Fatalf("content type = %q, want HTML", ct) | ||
| 47 | } | ||
| 48 | |||
| 49 | // Without the probe, the path is not a page: the styled 404 is unchanged. | ||
| 50 | if code, body := f.get("/" + f.repo); code != http.StatusNotFound || !strings.Contains(body, "Not found") { | ||
| 51 | t.Fatalf("/%s = %d, want the styled 404", f.repo, code) | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | // The tag is fetched without credentials, so answering for a private repository | ||
| 56 | // would confirm its existence to anyone who guessed the name. | ||
| 57 | func TestPrivateRepositoryDoesNotAdvertiseAGoModule(t *testing.T) { | ||
| 58 | t.Parallel() | ||
| 59 | f := newFixture(t) | ||
| 60 | if code, _ := f.get("/" + f.repo + "?go-get=1"); code != http.StatusNotFound { | ||
| 61 | t.Fatalf("private repository answered the discovery probe: %d", code) | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | // Nothing names a repository, so nothing is answered. An unknown name is a 404 | ||
| 66 | // like every other unknown path. | ||
| 67 | func TestGoImportIgnoresPathsThatNameNoRepository(t *testing.T) { | ||
| 68 | t.Parallel() | ||
| 69 | f := newFixture(t) | ||
| 70 | f.publish(f.repo) | ||
| 71 | for _, path := range []string{ | ||
| 72 | "/no-such-repository?go-get=1", | ||
| 73 | "/no-such-repository/pkg?go-get=1", | ||
| 74 | "/" + f.repo + "?go-get=0", | ||
| 75 | } { | ||
| 76 | if code, _ := f.get(path); code != http.StatusNotFound { | ||
| 77 | t.Fatalf("%s = %d, want 404", path, code) | ||
| 78 | } | ||
| 79 | } | ||
| 80 | } | ||