115 lines
3.7 KiB
Go
115 lines
3.7 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
// Option changes FetchCommit.
|
|
type Option func(*fetchOptions)
|
|
|
|
type fetchOptions struct{ allowFile bool }
|
|
|
|
// AllowFileRemote lets FetchCommit take a file:// remote. It is for a test's local repository; production
|
|
// code does not pass it, and FetchCommit then refuses anything but ssh://.
|
|
func AllowFileRemote() Option { return func(o *fetchOptions) { o.allowFile = true } }
|
|
|
|
var (
|
|
commitOK = regexp.MustCompile(`^[0-9a-f]{40}$`)
|
|
refOK = regexp.MustCompile(`^refs/[A-Za-z0-9._/-]+$`)
|
|
)
|
|
|
|
// FetchCommit fetches one commit of a forge repository into dir, an empty directory, and checks it out detached
|
|
// (novox/hq ADR 0466 §4, §5; issue 1474). remote is ssh://<ssh name>/<owner>/<repo>.git, the ssh name being the
|
|
// one the build module's templates set up (ADR 0409 §1); commit is a full 40-hex sha; ref is optional, and names
|
|
// the pull request's head (refs/pull/<n>/head) when the commit is only there. Every git command runs through run
|
|
// as `git -C dir ...` with GIT_TERMINAL_PROMPT=0 and no credential helper, so nothing is asked for and no
|
|
// credential is used. A remote that is not ssh://, or one with user information, and a commit that is not 40
|
|
// hex digits are refused before anything runs.
|
|
func FetchCommit(ctx context.Context, run forgekey.Runner, dir, remote, commit, ref string, opts ...Option) error {
|
|
var o fetchOptions
|
|
for _, opt := range opts {
|
|
opt(&o)
|
|
}
|
|
if err := checkFetch(o, dir, remote, commit, ref); err != nil {
|
|
return err
|
|
}
|
|
env := []string{"GIT_TERMINAL_PROMPT=0"}
|
|
git := func(args ...string) (string, error) {
|
|
full := append([]string{"-c", "credential.helper=", "-C", dir}, args...)
|
|
r := run(ctx, env, "git", full...)
|
|
if r.Status != 0 || r.Err != "" {
|
|
return "", fmt.Errorf("git %s: %s", args[0], firstLine(r.Err+" "+r.Stderr))
|
|
}
|
|
return r.Stdout, nil
|
|
}
|
|
if _, err := git("init", "-q"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := git("remote", "add", "origin", remote); err != nil {
|
|
return err
|
|
}
|
|
if _, err := git("fetch", "--no-tags", "--depth", "1", "origin", commit); err != nil {
|
|
if ref == "" {
|
|
return err
|
|
}
|
|
if _, rerr := git("fetch", "--no-tags", "origin", ref); rerr != nil {
|
|
return fmt.Errorf("%v; then the ref %s: %w", err, ref, rerr)
|
|
}
|
|
if _, verr := git("cat-file", "-e", commit+"^{commit}"); verr != nil {
|
|
return fmt.Errorf("the commit %s is not in %s: %w", commit, ref, verr)
|
|
}
|
|
}
|
|
if _, err := git("checkout", "-q", "--detach", commit); err != nil {
|
|
return err
|
|
}
|
|
out, err := git("rev-parse", "HEAD")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if head := strings.TrimSpace(out); head != commit {
|
|
return fmt.Errorf("the checkout is at %s, not at the commit %s", head, commit)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkFetch(o fetchOptions, dir, remote, commit, ref string) error {
|
|
if dir == "" {
|
|
return fmt.Errorf("buildstep: no workspace directory")
|
|
}
|
|
if !commitOK.MatchString(commit) {
|
|
return fmt.Errorf("buildstep: %q is not a full commit (40 hex digits)", commit)
|
|
}
|
|
if ref != "" && !refOK.MatchString(ref) {
|
|
return fmt.Errorf("buildstep: %q is not a ref (refs/...)", ref)
|
|
}
|
|
u, err := url.Parse(remote)
|
|
if err != nil {
|
|
return fmt.Errorf("buildstep: the remote is not a URL: %w", err)
|
|
}
|
|
if u.User != nil {
|
|
return fmt.Errorf("buildstep: the remote carries user information; the key is the credential")
|
|
}
|
|
switch {
|
|
case u.Scheme == "ssh" && u.Host != "":
|
|
case u.Scheme == "file" && o.allowFile:
|
|
default:
|
|
return fmt.Errorf("buildstep: the remote %q is not ssh://", remote)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func firstLine(s string) string {
|
|
for _, l := range strings.Split(s, "\n") {
|
|
if l = strings.TrimSpace(l); l != "" {
|
|
return l
|
|
}
|
|
}
|
|
return ""
|
|
}
|