Fresh repository history for elektrine/tarakan-client hosted at https://git.elektrine.com/elektrine/tarakan-client.
175 lines
5.4 KiB
Go
175 lines
5.4 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/atomine-elektrine/tarakan-client/internal/api"
|
|
repoctx "github.com/atomine-elektrine/tarakan-client/internal/context"
|
|
)
|
|
|
|
// worktreeForTask returns a git repository root that contains the job's pinned
|
|
// commit. Uses the local clone when origin matches; otherwise clones from the
|
|
// job's host (GitHub, Tarakan-hosted, or CanonicalURL) into a temp directory.
|
|
//
|
|
// apiBase is TARAKAN_URL (e.g. http://localhost:4000) used for Tarakan-hosted
|
|
// clones when CanonicalURL points at the public host but git is served locally.
|
|
// report is optional; non-empty lines are status updates for the TUI.
|
|
func worktreeForTask(local repoctx.Info, task api.Task, apiBase string, report func(string)) (root string, cleanup func(), err error) {
|
|
if report == nil {
|
|
report = func(string) {}
|
|
}
|
|
cleanup = func() {}
|
|
if len(task.CommitSHA) != 40 {
|
|
return "", cleanup, fmt.Errorf("job %d has no full commit SHA to pin", task.ID)
|
|
}
|
|
slug := task.Repository.Slug()
|
|
if local.IsGit && localMatchesTask(local, task) {
|
|
report("Using local clone of " + slug + " at " + local.Root)
|
|
return local.Root, cleanup, nil
|
|
}
|
|
|
|
owner := task.Repository.Owner
|
|
name := task.Repository.Name
|
|
if owner == "" || name == "" {
|
|
return "", cleanup, fmt.Errorf("job %d has no repository identity", task.ID)
|
|
}
|
|
|
|
remote, err := cloneRemoteURL(task.Repository, apiBase)
|
|
if err != nil {
|
|
return "", cleanup, err
|
|
}
|
|
|
|
report("Cloning " + slug + " from " + remote + "…")
|
|
base, err := os.MkdirTemp("", "tarakan-pickup-")
|
|
if err != nil {
|
|
return "", cleanup, fmt.Errorf("create temp worktree: %w", err)
|
|
}
|
|
cleanup = func() { _ = os.RemoveAll(base) }
|
|
|
|
repoDir := filepath.Join(base, "repository")
|
|
if err := runGit("", "clone", "--filter=blob:none", "--no-checkout", remote, repoDir); err != nil {
|
|
cleanup()
|
|
return "", func() {}, fmt.Errorf("clone %s for job %d: %w\n(hint: cd into a clone of %s/%s and re-run)", remote, task.ID, err, owner, name)
|
|
}
|
|
report("Fetching pinned commit " + shortSHA(task.CommitSHA) + "…")
|
|
if err := runGit(repoDir, "fetch", "--depth", "1", "origin", task.CommitSHA); err != nil {
|
|
if err2 := runGit(repoDir, "fetch", "origin", task.CommitSHA); err2 != nil {
|
|
cleanup()
|
|
return "", func() {}, fmt.Errorf("fetch commit %s: %w", shortSHA(task.CommitSHA), err2)
|
|
}
|
|
}
|
|
report("Checking out " + shortSHA(task.CommitSHA) + "…")
|
|
if err := runGit(repoDir, "checkout", "--detach", "--force", task.CommitSHA); err != nil {
|
|
cleanup()
|
|
return "", func() {}, fmt.Errorf("checkout %s: %w", shortSHA(task.CommitSHA), err)
|
|
}
|
|
report("Worktree ready for " + slug + " @ " + shortSHA(task.CommitSHA))
|
|
return repoDir, cleanup, nil
|
|
}
|
|
|
|
func localMatchesTask(local repoctx.Info, task api.Task) bool {
|
|
owner, name, ok := local.RemoteSlug()
|
|
if !ok {
|
|
owner, name = local.GitHubOwner, local.GitHubName
|
|
if owner == "" || name == "" {
|
|
return false
|
|
}
|
|
}
|
|
if !strings.EqualFold(owner, task.Repository.Owner) || !strings.EqualFold(name, task.Repository.Name) {
|
|
return false
|
|
}
|
|
taskHost := normalizeTaskHost(task.Repository.Host)
|
|
localHost := strings.ToLower(local.Host)
|
|
if taskHost == "" || localHost == "" {
|
|
// Owner/name match is enough when one side lacks host.
|
|
return true
|
|
}
|
|
if taskHost == localHost {
|
|
return true
|
|
}
|
|
// Local clone of hosted repo via localhost vs public tarakan.lol.
|
|
if isTarakanHost(taskHost) && (isTarakanHost(localHost) || isLoopback(localHost)) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func cloneRemoteURL(repo api.Repository, apiBase string) (string, error) {
|
|
host := normalizeTaskHost(repo.Host)
|
|
owner, name := repo.Owner, repo.Name
|
|
|
|
// Prefer live Tarakan base for hosted repos (dev often uses localhost while
|
|
// canonical_url says https://tarakan.lol/…).
|
|
if isTarakanHost(host) || host == "" && strings.Contains(strings.ToLower(repo.CanonicalURL), "tarakan") {
|
|
if base := strings.TrimRight(strings.TrimSpace(apiBase), "/"); base != "" {
|
|
return base + "/" + owner + "/" + name + ".git", nil
|
|
}
|
|
}
|
|
|
|
if u := strings.TrimSpace(repo.CanonicalURL); u != "" {
|
|
u = strings.TrimRight(u, "/")
|
|
if !strings.HasSuffix(strings.ToLower(u), ".git") {
|
|
u += ".git"
|
|
}
|
|
if _, err := url.Parse(u); err == nil {
|
|
return u, nil
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case host == "" || host == "github.com":
|
|
return "https://github.com/" + owner + "/" + name + ".git", nil
|
|
case isTarakanHost(host):
|
|
return "https://tarakan.lol/" + owner + "/" + name + ".git", nil
|
|
default:
|
|
return "", fmt.Errorf("job host %q is not supported for auto-clone; clone %s/%s locally and re-run", repo.Host, owner, name)
|
|
}
|
|
}
|
|
|
|
func normalizeTaskHost(host string) string {
|
|
h := strings.ToLower(strings.TrimSpace(host))
|
|
switch h {
|
|
case "github", "www.github.com":
|
|
return "github.com"
|
|
case "tarakan", "www.tarakan.lol":
|
|
return "tarakan.lol"
|
|
default:
|
|
return h
|
|
}
|
|
}
|
|
|
|
func isTarakanHost(host string) bool {
|
|
h := normalizeTaskHost(host)
|
|
return h == "tarakan.lol"
|
|
}
|
|
|
|
func isLoopback(host string) bool {
|
|
h := strings.ToLower(host)
|
|
return h == "localhost" || h == "127.0.0.1" || h == "::1"
|
|
}
|
|
|
|
func runGit(dir string, args ...string) error {
|
|
cmd := exec.Command("git", args...)
|
|
if dir != "" {
|
|
cmd.Dir = dir
|
|
}
|
|
cmd.Env = append(os.Environ(),
|
|
"GIT_TERMINAL_PROMPT=0",
|
|
"GIT_CONFIG_NOSYSTEM=1",
|
|
"GCM_INTERACTIVE=never",
|
|
)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
msg := strings.TrimSpace(string(output))
|
|
if msg == "" {
|
|
return err
|
|
}
|
|
return fmt.Errorf("%w: %s", err, msg)
|
|
}
|
|
return nil
|
|
}
|