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>
101 lines
2.8 KiB
Go
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)
|
|
}
|