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