You've already forked agentic-coding-workflow
90 lines
3.0 KiB
Go
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
|
|
}
|