Initial commit on Forgejo
Fresh repository history for elektrine/tarakan-client hosted at https://git.elektrine.com/elektrine/tarakan-client.
This commit is contained in:
commit
58b29a7f84
70 changed files with 12994 additions and 0 deletions
240
internal/context/context.go
Normal file
240
internal/context/context.go
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
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
|
||||
}
|
||||
136
internal/context/context_test.go
Normal file
136
internal/context/context_test.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package repoctx
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDiscoverOutsideGit(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
info := Discover(directory)
|
||||
|
||||
if info.Root != directory {
|
||||
t.Fatalf("root = %q, want %q", info.Root, directory)
|
||||
}
|
||||
if info.Name != filepath.Base(directory) {
|
||||
t.Fatalf("name = %q, want %q", info.Name, filepath.Base(directory))
|
||||
}
|
||||
if info.IsGit {
|
||||
t.Fatal("temporary directory unexpectedly detected as a Git repository")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverGitRepository(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git is not installed")
|
||||
}
|
||||
|
||||
directory := t.TempDir()
|
||||
command := exec.Command("git", "init", "-b", "main", directory)
|
||||
if output, err := command.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git init: %v: %s", err, output)
|
||||
}
|
||||
command = exec.Command("git", "-C", directory, "remote", "add", "origin", "git@github.com:tarakan-lol/client.git")
|
||||
if output, err := command.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git remote add: %v: %s", err, output)
|
||||
}
|
||||
|
||||
info := Discover(directory)
|
||||
if !info.IsGit {
|
||||
t.Fatal("Git repository was not detected")
|
||||
}
|
||||
if info.Branch != "main" {
|
||||
t.Fatalf("branch = %q, want main", info.Branch)
|
||||
}
|
||||
if info.GitHubOwner != "tarakan-lol" || info.GitHubName != "client" {
|
||||
t.Fatalf("GitHub repository = %q/%q", info.GitHubOwner, info.GitHubName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGitHubRemote(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
remote string
|
||||
owner string
|
||||
repo string
|
||||
ok bool
|
||||
}{
|
||||
{name: "HTTPS", remote: "https://github.com/openai/codex.git", owner: "openai", repo: "codex", ok: true},
|
||||
{name: "SSH URL", remote: "ssh://git@github.com/openai/codex.git", owner: "openai", repo: "codex", ok: true},
|
||||
{name: "SCP SSH", remote: "git@github.com:openai/codex.git", owner: "openai", repo: "codex", ok: true},
|
||||
{name: "git protocol", remote: "git://github.com/openai/codex", owner: "openai", repo: "codex", ok: true},
|
||||
{name: "lookalike host", remote: "https://github.com.example.org/openai/codex.git", ok: false},
|
||||
{name: "GitLab", remote: "git@gitlab.com:openai/codex.git", ok: false},
|
||||
{name: "Tarakan hosted", remote: "https://tarakan.lol/max/elektrine.git", ok: false},
|
||||
{name: "nested path", remote: "https://github.com/one/two/three.git", ok: false},
|
||||
{name: "file scheme", remote: "file://github.com/openai/codex.git", ok: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
owner, repo, ok := ParseGitHubRemote(test.remote)
|
||||
if owner != test.owner || repo != test.repo || ok != test.ok {
|
||||
t.Fatalf("ParseGitHubRemote(%q) = %q, %q, %v", test.remote, owner, repo, ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRemoteMultiHost(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
remote string
|
||||
host string
|
||||
owner string
|
||||
repo string
|
||||
ok bool
|
||||
}{
|
||||
{name: "GitHub HTTPS", remote: "https://github.com/openai/codex.git", host: "github.com", owner: "openai", repo: "codex", ok: true},
|
||||
{name: "Tarakan HTTPS", remote: "https://tarakan.lol/max/elektrine.git", host: "tarakan.lol", owner: "max", repo: "elektrine", ok: true},
|
||||
{name: "Tarakan no .git", remote: "https://tarakan.lol/max/elektrine", host: "tarakan.lol", owner: "max", repo: "elektrine", ok: true},
|
||||
{name: "localhost dev", remote: "http://localhost:4000/max/elektrine.git", host: "localhost", owner: "max", repo: "elektrine", ok: true},
|
||||
{name: "SCP Tarakan", remote: "git@tarakan.lol:max/elektrine.git", host: "tarakan.lol", owner: "max", repo: "elektrine", ok: true},
|
||||
{name: "nested path", remote: "https://tarakan.lol/one/two/three.git", ok: false},
|
||||
{name: "empty", remote: "", ok: false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
host, owner, repo, ok := ParseRemote(test.remote)
|
||||
if host != test.host || owner != test.owner || repo != test.repo || ok != test.ok {
|
||||
t.Fatalf("ParseRemote(%q) = %q, %q, %q, %v; want %q, %q, %q, %v",
|
||||
test.remote, host, owner, repo, ok, test.host, test.owner, test.repo, test.ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverTarakanRemote(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git is not installed")
|
||||
}
|
||||
directory := t.TempDir()
|
||||
if out, err := exec.Command("git", "init", "-b", "main", directory).CombinedOutput(); err != nil {
|
||||
t.Fatalf("git init: %v: %s", err, out)
|
||||
}
|
||||
if out, err := exec.Command("git", "-C", directory, "remote", "add", "origin", "https://tarakan.lol/max/elektrine.git").CombinedOutput(); err != nil {
|
||||
t.Fatalf("git remote add: %v: %s", err, out)
|
||||
}
|
||||
info := Discover(directory)
|
||||
if !info.IsGit {
|
||||
t.Fatal("expected git repo")
|
||||
}
|
||||
if info.Host != "tarakan.lol" || info.Owner != "max" || info.Repo != "elektrine" {
|
||||
t.Fatalf("remote = %q/%q/%q", info.Host, info.Owner, info.Repo)
|
||||
}
|
||||
if owner, name, ok := info.RemoteSlug(); !ok || owner != "max" || name != "elektrine" {
|
||||
t.Fatalf("RemoteSlug = %q/%q %v", owner, name, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactRemoteCredentials(t *testing.T) {
|
||||
remote := redactRemote("https://secret-token@github.example/repository.git?token=also-secret")
|
||||
if remote != "https://github.example/repository.git" {
|
||||
t.Fatalf("redacted remote = %q", remote)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue