gitmote / internal / runner / git.go · history · branches
2304 bytes · raw
package runner
import (
"context"
"fmt"
"net/url"
"os"
"os/exec"
"strings"
)
// GitCloner clones over the normal git-HTTP path with a scoped token — the same
// authenticated path any client uses, so there is no CI-only backdoor in the git
// handler (tasks/16-ci.md Stage 0 #4).
type GitCloner struct{}
// Clone fetches repo into dest and checks out the exact sha. It authenticates by
// embedding the token as the HTTP password (the username is ignored by the guard
// when a password is present). When ref names a branch, the SHA is checked out
// onto that branch so the engine can resolve github.ref; otherwise it is a
// detached checkout.
func (GitCloner) Clone(ctx context.Context, baseURL, repo, sha, ref, token, dest string) error {
u, err := url.Parse(baseURL)
if err != nil {
return fmt.Errorf("parse base url %q: %w", baseURL, err)
}
u.User = url.UserPassword("x-access-token", token)
u.Path = "/" + repo
if out, err := git(ctx, "", "clone", "--quiet", u.String(), dest); err != nil {
return fmt.Errorf("git clone: %w: %s", err, out)
}
// Check out the pushed SHA precisely — the branch tip may have moved on by
// the time the runner clones (a newer push), so never trust HEAD. Put it on
// the pushed branch (checkout -B) so the workflow sees a real ref, not a
// detached HEAD.
if branch := strings.TrimPrefix(ref, "refs/heads/"); branch != "" && branch != ref {
if out, err := git(ctx, dest, "checkout", "--quiet", "-B", branch, sha); err != nil {
return fmt.Errorf("git checkout -B %s %s: %w: %s", branch, sha, err, out)
}
return nil
}
if out, err := git(ctx, dest, "-c", "advice.detachedHead=false", "checkout", "--quiet", sha); err != nil {
return fmt.Errorf("git checkout %s: %w: %s", sha, err, out)
}
return nil
}
// git runs a git command in dir (repo root; "" for none), returning combined
// output. GIT_TERMINAL_PROMPT=0 makes a bad credential fail fast instead of
// hanging on a prompt; an empty credential.helper ignores any host helper.
func git(ctx context.Context, dir string, args ...string) ([]byte, error) {
full := append([]string{"-c", "credential.helper="}, args...)
cmd := exec.CommandContext(ctx, "git", full...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
return cmd.CombinedOutput()
}