Files
2026-06-22 08:55:57 +08:00

90 lines
3.0 KiB
Go

package vcs
import (
"net/url"
"strings"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)
// ErrUnsupportedHost is returned by ParseRepoRef when the remote points at a
// host other than the one the v1 Gitea provider serves.
var ErrUnsupportedHost = errs.New(errs.CodeInvalidInput, "vcs: unsupported git host for PR operations")
// ParseRepoRef derives owner/name from a workspace GitRemoteURL. It accepts:
//
// https://host/owner/name(.git)
// http://host/owner/name(.git)
// ssh://git@host/owner/name(.git)
// git@host:owner/name(.git)
//
// wantHost is the bare host the provider serves (e.g. "git.jerryyan.net"); when
// non-empty the remote host must match it (port stripped, case-insensitive),
// otherwise ErrUnsupportedHost is returned. wantHost empty skips the host
// check (host-agnostic parse, used in tests).
func ParseRepoRef(remoteURL, wantHost string) (RepoRef, error) {
remoteURL = strings.TrimSpace(remoteURL)
if remoteURL == "" {
return RepoRef{}, errs.New(errs.CodeInvalidInput, "vcs: empty remote url")
}
host, path, err := splitHostPath(remoteURL)
if err != nil {
return RepoRef{}, err
}
if wantHost != "" && !sameHost(host, wantHost) {
return RepoRef{}, ErrUnsupportedHost
}
owner, name, err := splitOwnerName(path)
if err != nil {
return RepoRef{}, err
}
return RepoRef{Owner: owner, Name: name}, nil
}
// splitHostPath extracts the bare host and the repo path from a remote URL,
// handling both scheme URLs and the scp-like git@host:owner/name form.
func splitHostPath(remoteURL string) (host, path string, err error) {
// scp-like: git@host:owner/name.git (no scheme, single colon before path)
if !strings.Contains(remoteURL, "://") && strings.Contains(remoteURL, "@") && strings.Contains(remoteURL, ":") {
at := strings.Index(remoteURL, "@")
rest := remoteURL[at+1:]
colon := strings.Index(rest, ":")
if colon < 0 {
return "", "", errs.New(errs.CodeInvalidInput, "vcs: malformed scp remote url")
}
return rest[:colon], rest[colon+1:], nil
}
u, perr := url.Parse(remoteURL)
if perr != nil || u.Host == "" {
return "", "", errs.Wrap(perr, errs.CodeInvalidInput, "vcs: cannot parse remote url")
}
return u.Hostname(), u.Path, nil
}
// splitOwnerName turns "/owner/name.git" or "owner/name.git" into (owner, name).
func splitOwnerName(path string) (owner, name string, err error) {
path = strings.Trim(path, "/")
path = strings.TrimSuffix(path, ".git")
parts := strings.Split(path, "/")
if len(parts) < 2 || parts[0] == "" || parts[len(parts)-1] == "" {
return "", "", errs.New(errs.CodeInvalidInput, "vcs: remote url must contain owner/name")
}
// Gitea repos are owner/name; deeper paths are not valid repo refs.
return parts[0], parts[len(parts)-1], nil
}
// sameHost compares hosts case-insensitively, ignoring any :port suffix.
func sameHost(a, b string) bool {
return strings.EqualFold(stripPort(a), stripPort(b))
}
func stripPort(h string) string {
if i := strings.LastIndex(h, ":"); i >= 0 {
return h[:i]
}
return h
}