A go vrepo serves the GOPROXY protocol at /go/<vrepo>/. Its upstreams
are ordered rules matched by module path (GOPRIVATE-style patterns);
each one relays to another module proxy, or builds .info/.mod/.zip,
@v/list and @latest itself from a git repository: through the Gitea
or GitLab REST API, or over git smart-HTTP (go-git) for any other
host, without a git binary. Module layout follows the go command:
<dir>/vX.Y.Z tags, /vN major subdirectories, +incompatible, nested
modules and vendor/ left out of zips. go.mod is served as committed,
so forks keeping their upstream module path work through replace.
Repo templates map captured pattern elements to repository paths
({1}, {2}...). vanity_hosts answers ?go-get=1 with a go-import meta
tag pointing at this proxy or at the repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
5.6 KiB
Go
165 lines
5.6 KiB
Go
package goproxy
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-git/go-billy/v5/memfs"
|
|
"github.com/go-git/go-billy/v5/util"
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/plumbing"
|
|
"github.com/go-git/go-git/v5/plumbing/format/pktline"
|
|
"github.com/go-git/go-git/v5/plumbing/object"
|
|
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
|
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
|
"github.com/go-git/go-git/v5/plumbing/transport/server"
|
|
"github.com/go-git/go-git/v5/storage/memory"
|
|
|
|
"tiny-proxy/internal/config"
|
|
)
|
|
|
|
// newGitServer builds a real repository from the fake repo's history (one
|
|
// commit per fakeCommit, on main) and serves it over smart HTTP at
|
|
// /<name>.git, using go-git's own upload-pack server. Tags become
|
|
// annotated tags, so peeling is exercised. It returns the server and the
|
|
// real commit hashes, in fakeRepo order.
|
|
func newGitServer(t *testing.T, name string, fr *fakeRepo) (*httptest.Server, []string) {
|
|
t.Helper()
|
|
st := memory.NewStorage()
|
|
fs := memfs.New()
|
|
r, err := git.InitWithOptions(st, fs, git.InitOptions{DefaultBranch: plumbing.NewBranchReferenceName("main")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wt, err := r.Worktree()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
byFake := map[string]plumbing.Hash{}
|
|
var hashes []string
|
|
for _, c := range fr.commits {
|
|
for _, p := range sortedKeys(c.files) {
|
|
if err := util.WriteFile(fs, p, []byte(c.files[p]), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := wt.Add(p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
sig := &object.Signature{Name: "t", Email: "t@test", When: c.time}
|
|
h, err := wt.Commit("commit", &git.CommitOptions{Author: sig, Committer: sig})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
byFake[c.hash] = h
|
|
hashes = append(hashes, h.String())
|
|
}
|
|
for name, fake := range fr.tags {
|
|
sig := &object.Signature{Name: "t", Email: "t@test", When: t1}
|
|
if _, err := r.CreateTag(name, byFake[fake], &git.CreateTagOptions{Tagger: sig, Message: name}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
ep, err := transport.NewEndpoint("file:///" + name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := server.NewServer(server.MapLoader{ep.String(): st})
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /"+name+".git/info/refs", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("service") != transport.UploadPackServiceName {
|
|
http.Error(w, "dumb http not supported", http.StatusForbidden)
|
|
return
|
|
}
|
|
sess, err := srv.NewUploadPackSession(ep, nil)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
ar, err := sess.AdvertisedReferencesContext(r.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
ar.Prefix = [][]byte{[]byte("# service=" + transport.UploadPackServiceName), pktline.Flush}
|
|
w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
|
|
ar.Encode(w)
|
|
})
|
|
mux.HandleFunc("POST /"+name+".git/git-upload-pack", func(w http.ResponseWriter, r *http.Request) {
|
|
req := packp.NewUploadPackRequest()
|
|
if err := req.Decode(r.Body); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
sess, err := srv.NewUploadPackSession(ep, nil)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
resp, err := sess.UploadPack(r.Context(), req)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
|
|
resp.Encode(w)
|
|
})
|
|
ts := httptest.NewServer(mux)
|
|
t.Cleanup(ts.Close)
|
|
return ts, hashes
|
|
}
|
|
|
|
func TestGitSource(t *testing.T) {
|
|
ts, hashes := newGitServer(t, "lib", newLibRepo())
|
|
h := newTestHandler(t, goRepo(
|
|
config.UpstreamConf{Kind: config.GoKindGit, URL: ts.URL, Modules: []string{"go.test/*"}, Repo: "{1}.git"},
|
|
))
|
|
const base = "/go/go/go.test"
|
|
|
|
wantBody(t, h, base+"/lib/@v/list", "v1.0.0\nv1.1.0-rc.1\n")
|
|
wantBody(t, h, base+"/lib/@latest", infoJSON("v1.0.0", t1))
|
|
wantBody(t, h, base+"/lib/@v/v1.0.0.mod", "module example.com/lib\n")
|
|
|
|
// The branch head is untagged: a pseudo-version built from the real
|
|
// hash, which then resolves for .mod and .zip.
|
|
pseudo := "v0.0.0-20250104030405-" + hashes[2][:12]
|
|
wantBody(t, h, base+"/lib/@v/main.info", infoJSON(pseudo, t3))
|
|
wantBody(t, h, base+"/lib/@v/"+pseudo+".mod", "module example.com/lib\n")
|
|
w := serve(t, h, base+"/lib/@v/"+pseudo+".zip")
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("pseudo zip = %d %s", w.Code, w.Body)
|
|
}
|
|
p := "go.test/lib@" + pseudo + "/"
|
|
if got, want := zipNames(t, w.Body.Bytes()), []string{p + "LICENSE", p + "docs/README.md", p + "go.mod", p + "lib.go"}; !slices.Equal(got, want) {
|
|
t.Errorf("zip files = %v, want %v", got, want)
|
|
}
|
|
|
|
wantBody(t, h, base+"/lib/v2/@v/v2.0.0.mod", "module example.com/lib/v2\n")
|
|
wantBody(t, h, base+"/lib/sub/@v/list", "v0.1.0\n")
|
|
wantStatus(t, h, base+"/missing/@v/list", http.StatusNotFound)
|
|
}
|
|
|
|
func TestGitSourceAbbreviatedHash(t *testing.T) {
|
|
ts, hashes := newGitServer(t, "lib", newLibRepo())
|
|
src, err := newGitSource(http.DefaultClient, ts.URL, "lib.git", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := t.Context()
|
|
// A ref tip resolves by any prefix; an older commit once it was seen.
|
|
c, err := src.Commit(ctx, hashes[2][:8])
|
|
if err != nil || c.Hash != hashes[2] {
|
|
t.Fatalf("tip by prefix = %+v, %v", c, err)
|
|
}
|
|
if _, err := src.Commit(ctx, strings.Repeat("0", 12)); err == nil {
|
|
t.Error("unknown abbreviated hash resolved")
|
|
}
|
|
if c, err := src.Commit(ctx, "v1.0.0"); err != nil || c.Hash != hashes[0] {
|
|
t.Errorf("annotated tag = %+v, %v (want peeled %s)", c, err, hashes[0])
|
|
}
|
|
}
|