Files
MRLokopandClaude Opus 5.5 42abe48da1 Add a Go module proxy with per-module routing rules
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>
2026-10-16 09:53:25 +03:00

101 lines
2.8 KiB
Go

package goproxy
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
// gitlabSource reads a repository through the GitLab REST API (/api/v4).
// The project is addressed by its URL-encoded full path, so nested groups
// work.
type gitlabSource struct {
client *http.Client
api string // .../api/v4/projects/<group%2Fname>
hdr http.Header
}
func newGitlabSource(c *http.Client, base, repo, password string) (*gitlabSource, error) {
repo = strings.TrimSuffix(strings.Trim(repo, "/"), ".git")
if !strings.Contains(repo, "/") {
return nil, fmt.Errorf("gitlab repo %q is not group/name", repo)
}
s := &gitlabSource{
client: c,
api: strings.TrimSuffix(base, "/") + "/api/v4/projects/" + url.PathEscape(repo),
hdr: http.Header{},
}
// GitLab's API takes tokens as Bearer, not Basic; a preset
// Authorization also keeps the upstream client from adding Basic.
if password != "" {
s.hdr.Set("Authorization", "Bearer "+password)
}
return s, nil
}
func (s *gitlabSource) Tags(ctx context.Context) ([]tag, error) {
const perPage = 100
var tags []tag
for page := 1; ; page++ {
var batch []struct {
Name string `json:"name"`
Commit struct {
ID string `json:"id"`
} `json:"commit"`
}
if err := s.getJSON(ctx, "/repository/tags?per_page="+strconv.Itoa(perPage)+"&page="+strconv.Itoa(page), &batch); err != nil {
return nil, err
}
for _, t := range batch {
tags = append(tags, tag{Name: t.Name, Hash: t.Commit.ID})
}
if len(batch) < perPage {
return tags, nil
}
}
}
func (s *gitlabSource) Commit(ctx context.Context, rev string) (commit, error) {
if rev == "" {
var info struct {
DefaultBranch string `json:"default_branch"`
}
if err := s.getJSON(ctx, "", &info); err != nil {
return commit{}, err
}
if info.DefaultBranch == "" {
return commit{}, errNotFound // empty repository
}
rev = info.DefaultBranch
}
var c struct {
ID string `json:"id"`
CommittedDate time.Time `json:"committed_date"`
}
if err := s.getJSON(ctx, "/repository/commits/"+url.PathEscape(rev), &c); err != nil {
return commit{}, err
}
return commit{Hash: c.ID, Time: c.CommittedDate}, nil
}
func (s *gitlabSource) ReadFile(ctx context.Context, hash, name string) ([]byte, error) {
return getBytes(ctx, s.client, s.api+"/repository/files/"+url.PathEscape(name)+"/raw?ref="+url.QueryEscape(hash), s.hdr, apiLimit)
}
func (s *gitlabSource) Tree(ctx context.Context, hash string) (tree, error) {
return downloadArchive(ctx, s.client, s.api+"/repository/archive.zip?sha="+url.QueryEscape(hash), s.hdr)
}
func (s *gitlabSource) getJSON(ctx context.Context, suffix string, v any) error {
body, err := getBytes(ctx, s.client, s.api+suffix, s.hdr, apiLimit)
if err != nil {
return err
}
return json.Unmarshal(body, v)
}