Fresh repository history for elektrine/tarakan-client hosted at https://git.elektrine.com/elektrine/tarakan-client.
240 lines
6.7 KiB
Go
240 lines
6.7 KiB
Go
package repoctx
|
|
|
|
import (
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Info is the repository context attached to an agent session.
|
|
type Info struct {
|
|
Root string `json:"root"`
|
|
Name string `json:"name"`
|
|
Branch string `json:"branch,omitempty"`
|
|
Commit string `json:"commit,omitempty"`
|
|
CommitSHA string `json:"commit_sha,omitempty"`
|
|
IsGit bool `json:"is_git"`
|
|
Dirty bool `json:"dirty"`
|
|
Origin string `json:"origin,omitempty"`
|
|
|
|
// Remote identity (any supported host).
|
|
Host string `json:"host,omitempty"` // e.g. github.com, tarakan.lol
|
|
Owner string `json:"owner,omitempty"` // remote owner / handle
|
|
Repo string `json:"repo,omitempty"` // remote repository name
|
|
|
|
// GitHubOwner/GitHubName are kept for older call sites; set when Host is GitHub,
|
|
// and also mirrored from Owner/Repo for convenience when matching jobs.
|
|
GitHubOwner string `json:"github_owner,omitempty"`
|
|
GitHubName string `json:"github_name,omitempty"`
|
|
}
|
|
|
|
// Discover finds the Git repository containing path. Outside a Git repository,
|
|
// it still returns useful directory context.
|
|
func Discover(path string) Info {
|
|
absolute, err := filepath.Abs(path)
|
|
if err != nil {
|
|
absolute = path
|
|
}
|
|
|
|
info := Info{
|
|
Root: absolute,
|
|
Name: filepath.Base(absolute),
|
|
}
|
|
|
|
if root, ok := gitOutput(absolute, "rev-parse", "--show-toplevel"); ok {
|
|
info.Root = root
|
|
info.Name = filepath.Base(root)
|
|
info.IsGit = true
|
|
info.Branch, _ = gitOutput(root, "branch", "--show-current")
|
|
info.Commit, _ = gitOutput(root, "rev-parse", "--short", "HEAD")
|
|
info.CommitSHA, _ = gitOutput(root, "rev-parse", "HEAD")
|
|
if status, found := gitOutput(root, "status", "--porcelain", "--untracked-files=normal"); found {
|
|
info.Dirty = status != ""
|
|
}
|
|
if origin, found := gitOutput(root, "remote", "get-url", "origin"); found {
|
|
if host, owner, repo, ok := ParseRemote(origin); ok {
|
|
info.Host = host
|
|
info.Owner = owner
|
|
info.Repo = repo
|
|
info.Origin = redactRemote(origin)
|
|
if isGitHubHost(host) {
|
|
info.GitHubOwner = owner
|
|
info.GitHubName = repo
|
|
info.Origin = "https://github.com/" + owner + "/" + repo + ".git"
|
|
}
|
|
} else {
|
|
info.Origin = redactRemote(origin)
|
|
}
|
|
}
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
func redactRemote(remote string) string {
|
|
parsed, err := url.Parse(remote)
|
|
if err != nil || parsed.Scheme == "" {
|
|
if separator := strings.IndexByte(remote, ':'); separator >= 0 {
|
|
host := remote[:separator]
|
|
if at := strings.LastIndexByte(host, '@'); at >= 0 {
|
|
host = host[at+1:]
|
|
}
|
|
return host + ":" + remote[separator+1:]
|
|
}
|
|
return ""
|
|
}
|
|
parsed.User = nil
|
|
parsed.RawQuery = ""
|
|
parsed.Fragment = ""
|
|
return parsed.String()
|
|
}
|
|
|
|
// GitHubRepository returns origin's owner/name pair when origin points to
|
|
// GitHub. The second result is false for non-GitHub or malformed remotes.
|
|
func (i Info) GitHubRepository() (string, bool) {
|
|
if i.GitHubOwner == "" || i.GitHubName == "" {
|
|
return "", false
|
|
}
|
|
return i.GitHubOwner + "/" + i.GitHubName, true
|
|
}
|
|
|
|
// RemoteSlug is host-less owner/name when known.
|
|
func (i Info) RemoteSlug() (owner, name string, ok bool) {
|
|
if i.Owner != "" && i.Repo != "" {
|
|
return i.Owner, i.Repo, true
|
|
}
|
|
if i.GitHubOwner != "" && i.GitHubName != "" {
|
|
return i.GitHubOwner, i.GitHubName, true
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
// ParseGitHubRemote accepts GitHub HTTPS, SSH URL, and SCP-like remote forms.
|
|
func ParseGitHubRemote(remote string) (owner, name string, ok bool) {
|
|
host, owner, name, ok := ParseRemote(remote)
|
|
if !ok || !isGitHubHost(host) {
|
|
return "", "", false
|
|
}
|
|
return owner, name, true
|
|
}
|
|
|
|
// ParseRemote extracts host, owner, and repo from common git remote URLs
|
|
// (HTTPS, SSH, SCP-like) for GitHub, Tarakan-hosted, and similar forges.
|
|
func ParseRemote(remote string) (host, owner, name string, ok bool) {
|
|
remote = strings.TrimSpace(remote)
|
|
if remote == "" {
|
|
return "", "", "", false
|
|
}
|
|
|
|
// SCP-like: git@github.com:owner/repo.git or git@tarakan.lol:owner/repo.git
|
|
if !strings.Contains(remote, "://") {
|
|
if separator := strings.IndexByte(remote, ':'); separator >= 0 {
|
|
hostPart := remote[:separator]
|
|
if at := strings.LastIndexByte(hostPart, '@'); at >= 0 {
|
|
hostPart = hostPart[at+1:]
|
|
}
|
|
owner, name, pathOK := parseOwnerRepoPath(remote[separator+1:])
|
|
if !pathOK || !supportedHost(hostPart) {
|
|
return "", "", "", false
|
|
}
|
|
return normalizeHost(hostPart), owner, name, true
|
|
}
|
|
return "", "", "", false
|
|
}
|
|
|
|
parsed, err := url.Parse(remote)
|
|
if err != nil || !remoteScheme(parsed.Scheme) {
|
|
return "", "", "", false
|
|
}
|
|
hostPart := parsed.Hostname()
|
|
if !supportedHost(hostPart) {
|
|
return "", "", "", false
|
|
}
|
|
owner, name, pathOK := parseOwnerRepoPath(parsed.Path)
|
|
if !pathOK {
|
|
return "", "", "", false
|
|
}
|
|
return normalizeHost(hostPart), owner, name, true
|
|
}
|
|
|
|
func remoteScheme(scheme string) bool {
|
|
switch strings.ToLower(scheme) {
|
|
case "http", "https", "ssh", "git":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func supportedHost(host string) bool {
|
|
host = strings.ToLower(strings.TrimSpace(host))
|
|
if host == "" {
|
|
return false
|
|
}
|
|
// Explicit forges + loopback (local Tarakan-hosted clones).
|
|
if isGitHubHost(host) || isTarakanHost(host) || isLoopbackHost(host) {
|
|
return true
|
|
}
|
|
// Generic host.tld/owner/repo for future forges.
|
|
return strings.Contains(host, ".")
|
|
}
|
|
|
|
func isGitHubHost(host string) bool {
|
|
h := strings.ToLower(host)
|
|
return h == "github.com" || h == "www.github.com"
|
|
}
|
|
|
|
func isTarakanHost(host string) bool {
|
|
h := strings.ToLower(host)
|
|
return h == "tarakan.lol" || h == "www.tarakan.lol" || h == "tarakan"
|
|
}
|
|
|
|
func isLoopbackHost(host string) bool {
|
|
h := strings.ToLower(host)
|
|
return h == "localhost" || h == "127.0.0.1" || h == "::1"
|
|
}
|
|
|
|
func normalizeHost(host string) string {
|
|
h := strings.ToLower(strings.TrimSpace(host))
|
|
switch h {
|
|
case "www.github.com", "github":
|
|
return "github.com"
|
|
case "www.tarakan.lol", "tarakan":
|
|
return "tarakan.lol"
|
|
default:
|
|
return h
|
|
}
|
|
}
|
|
|
|
func parseOwnerRepoPath(path string) (owner, name string, ok bool) {
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) != 2 {
|
|
return "", "", false
|
|
}
|
|
owner = strings.TrimSpace(parts[0])
|
|
name = strings.TrimSuffix(strings.TrimSpace(parts[1]), ".git")
|
|
if owner == "" || name == "" || owner == "." || name == "." {
|
|
return "", "", false
|
|
}
|
|
return owner, name, true
|
|
}
|
|
|
|
// Current discovers context from the process working directory.
|
|
func Current() (Info, error) {
|
|
workingDirectory, err := os.Getwd()
|
|
if err != nil {
|
|
return Info{}, err
|
|
}
|
|
return Discover(workingDirectory), nil
|
|
}
|
|
|
|
func gitOutput(directory string, args ...string) (string, bool) {
|
|
commandArgs := append([]string{"-C", directory}, args...)
|
|
output, err := exec.Command("git", commandArgs...).Output()
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return strings.TrimSpace(string(output)), true
|
|
}
|