Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/handler/git_mirror.go
Simon Chrzanowski e9aa078a85 Mirror pkg:git tags and the submodules a commit pins
A pkg:git version can now be a tag as well as a full commit ID. With a
version, the submodules the commit records are mirrored at the commits it
pins, from the upstream.git hosts serving them; one no host serves is
skipped with a warning.
2026-11-07 17:29:03 +01:00

149 lines
4.8 KiB
Go

package handler
import (
"context"
"fmt"
"log/slog"
"net/url"
"path"
"strings"
"github.com/git-pkgs/proxy/internal/gitcache"
)
// maxSubmoduleDepth bounds how deeply Mirror follows submodules of
// submodules.
const maxSubmoduleDepth = 8
// Mirror caches the repository at location, a host and path such as
// github.com/acme/widgets as a pkg:git PURL names it, from the upstream.git
// host serving it. With version, a full commit ID or a tag, it caches that
// commit too, and the commits its submodules pin, from the upstream.git
// hosts serving them. A submodule no upstream.git host serves is skipped.
// Clones through /git are then served from those copies.
func (h *GitHandler) Mirror(ctx context.Context, location, version string) (gitcache.MirrorResult, error) {
return h.mirror(ctx, location, version, map[string]bool{}, 0)
}
func (h *GitHandler) mirror(ctx context.Context, location, version string, seen map[string]bool, depth int) (gitcache.MirrorResult, error) {
repo, err := h.mirrorRepository(location)
if err != nil {
return gitcache.MirrorResult{}, err
}
result, err := h.cache.Mirror(ctx, repo.key, repo.url, version, repo.upstream.fetchWants)
if err != nil {
return result, err
}
for _, sub := range result.Submodules {
subLocation, err := gitSubmoduleLocation(repo.url, sub.URL)
if err == nil {
_, err = h.mirrorRepository(subLocation)
}
if err != nil {
h.logger().Warn("git mirror: skipping submodule", "repository", location, "submodule", sub.Path, "url", sub.URL, "error", err)
continue
}
if seen[subLocation+"@"+sub.Commit] {
continue
}
seen[subLocation+"@"+sub.Commit] = true
if depth >= maxSubmoduleDepth {
return result, fmt.Errorf("submodule %s: nested more than %d deep", sub.Path, maxSubmoduleDepth)
}
subResult, err := h.mirror(ctx, subLocation, sub.Commit, seen, depth+1)
if err != nil {
return result, fmt.Errorf("submodule %s: %w", sub.Path, err)
}
result.Cached = result.Cached && subResult.Cached
result.Bytes += subResult.Bytes
}
return result, nil
}
func (h *GitHandler) logger() *slog.Logger {
if h.proxy != nil && h.proxy.Logger != nil {
return h.proxy.Logger
}
return slog.Default()
}
// gitSubmoduleLocation turns the URL .gitmodules gives a submodule into a
// location as Mirror takes it. A relative URL is resolved against the
// superproject's repoURL, as git does; ssh, git and scp-like URLs name the
// host and path the repository has there.
func gitSubmoduleLocation(repoURL, submoduleURL string) (string, error) {
var host, repoPath string
switch {
case strings.HasPrefix(submoduleURL, "./"), strings.HasPrefix(submoduleURL, "../"):
u, err := url.Parse(repoURL)
if err != nil {
return "", err
}
host, repoPath = u.Host, path.Join(u.Path, submoduleURL)
case strings.Contains(submoduleURL, "://"):
u, err := url.Parse(submoduleURL)
if err != nil {
return "", err
}
switch u.Scheme {
case "http", "https":
host = u.Host
case "ssh", "git", "git+ssh", "ssh+git":
host = u.Hostname()
default:
return "", fmt.Errorf("unsupported submodule URL %q", submoduleURL)
}
repoPath = u.Path
default:
// scp-like: [user@]host:path
hostPart, p, ok := strings.Cut(submoduleURL, ":")
if !ok || strings.Contains(hostPart, "/") {
return "", fmt.Errorf("unsupported submodule URL %q", submoduleURL)
}
_, host, _ = strings.Cut(hostPart, "@")
if host == "" {
host = hostPart
}
repoPath = p
}
repoPath = strings.Trim(path.Clean("/"+repoPath), "/")
if host == "" || repoPath == "" {
return "", fmt.Errorf("unsupported submodule URL %q", submoduleURL)
}
return strings.ToLower(host) + "/" + repoPath, nil
}
// mirrorRepository resolves location to a repository of the upstream.git
// host whose URL it starts with, the longest such URL if several do.
func (h *GitHandler) mirrorRepository(location string) (gitRepository, error) {
var best, bestPrefix, rest string
for name, upstream := range h.upstreams {
prefix := gitLocationPrefix(upstream.URL)
path, ok := strings.CutPrefix(location, prefix)
if prefix == "" || !ok || path == "" {
continue
}
if len(prefix) > len(bestPrefix) || (len(prefix) == len(bestPrefix) && name < best) {
best, bestPrefix, rest = name, prefix, path
}
}
if best == "" {
return gitRepository{}, fmt.Errorf("no upstream.git host serves %s", location)
}
repo, status, msg := h.repository(best + "/" + rest)
if status != 0 {
return gitRepository{}, fmt.Errorf("%s: %s", location, msg)
}
return repo, nil
}
// gitLocationPrefix is how a pkg:git PURL names the repositories under an
// upstream URL: its host and path, ending in a slash. Empty for a URL
// without a host.
func gitLocationPrefix(upstreamURL string) string {
u, err := url.Parse(upstreamURL)
if err != nil || u.Host == "" {
return ""
}
return strings.ToLower(u.Host) + strings.TrimSuffix(u.Path, "/") + "/"
}