package main import ( "archive/tar" "bufio" "bytes" "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strings" ) // resolve finds the commit ref points at in the repository at url. An empty // ref means the default branch; a full commit hash resolves to itself. func resolve(ctx context.Context, url, ref string) (string, error) { if fullRev.MatchString(ref) { return ref, nil } query := []string{"HEAD"} if ref != "" { query = []string{"refs/heads/" + ref, "refs/tags/" + ref, "refs/tags/" + ref + "^{}"} } out, err := git(ctx, "", append([]string{"ls-remote", url}, query...)...) if err != nil { return "", err } found := map[string]string{} sc := bufio.NewScanner(bytes.NewReader(out)) for sc.Scan() { rev, name, ok := strings.Cut(sc.Text(), "\t") if ok { found[name] = rev } } for _, name := range []string{"HEAD", "refs/heads/" + ref, "refs/tags/" + ref + "^{}", "refs/tags/" + ref} { if rev, ok := found[name]; ok { return rev, nil } } if ref == "" { return "", fmt.Errorf("%s has no default branch", url) } return "", fmt.Errorf("%s has no branch or tag named %q", url, ref) } // checkout makes rev of url available as a plain directory and returns it. // Checkouts are immutable and shared by everything that locks the same commit. func checkout(ctx context.Context, url, rev string) (string, error) { key := repoKey(url) dir := filepath.Join(dataDir(), "checkouts", key, rev) if _, err := os.Stat(dir); err == nil { return dir, nil } repo := filepath.Join(dataDir(), "repos", key+".git") if _, err := os.Stat(repo); err != nil { if _, err := git(ctx, "", "init", "--bare", "--quiet", repo); err != nil { return "", err } } if !hasCommit(ctx, repo, rev) { // Servers hand out branch and tag tips by hash; older commits may // need the whole history. if _, err := git(ctx, repo, "fetch", "--quiet", "--depth=1", url, rev); err != nil || !hasCommit(ctx, repo, rev) { args := []string{"fetch", "--quiet", url, "+refs/heads/*:refs/remotes/origin/*", "+refs/tags/*:refs/tags/*"} if _, err := os.Stat(filepath.Join(repo, "shallow")); err == nil { args = append(args, "--unshallow") } if _, err := git(ctx, repo, args...); err != nil { return "", err } } if !hasCommit(ctx, repo, rev) { return "", fmt.Errorf("%s has no commit %s", url, rev) } } tmp := fmt.Sprintf("%s.tmp-%d", dir, os.Getpid()) defer os.RemoveAll(tmp) if err := os.MkdirAll(tmp, 0o755); err != nil { return "", err } cmd := exec.CommandContext(ctx, "git", "-C", repo, "archive", "--format=tar", rev) stdout, err := cmd.StdoutPipe() if err != nil { return "", err } var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Start(); err != nil { return "", gitError(err, &stderr) } extractErr := untar(stdout, tmp) if err := cmd.Wait(); err != nil { return "", gitError(err, &stderr) } if extractErr != nil { return "", extractErr } if err := os.Rename(tmp, dir); err != nil { return "", err } return dir, nil } func hasCommit(ctx context.Context, repo, rev string) bool { _, err := git(ctx, repo, "cat-file", "-e", rev+"^{commit}") return err == nil } func untar(r io.Reader, dst string) error { tr := tar.NewReader(r) for { h, err := tr.Next() if errors.Is(err, io.EOF) { return nil } if err != nil { return err } name := filepath.Clean(h.Name) if filepath.IsAbs(name) || name == ".." || strings.HasPrefix(name, "../") { return fmt.Errorf("refusing to extract %q", h.Name) } path := filepath.Join(dst, name) switch h.Typeflag { case tar.TypeDir: err = os.MkdirAll(path, 0o755) case tar.TypeReg: err = writeFile(path, tr, os.FileMode(h.Mode)&0o755|0o644) case tar.TypeSymlink: err = os.Symlink(h.Linkname, path) } if err != nil { return err } } } func writeFile(path string, r io.Reader, mode os.FileMode) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) if err != nil { return err } if _, err := io.Copy(f, r); err != nil { f.Close() return err } return f.Close() } func repoKey(url string) string { sum := sha256.Sum256([]byte(url)) return hex.EncodeToString(sum[:8]) } func git(ctx context.Context, repo string, args ...string) ([]byte, error) { if repo != "" { args = append([]string{"-C", repo}, args...) } cmd := exec.CommandContext(ctx, "git", args...) // Fail instead of hanging on a password prompt nobody can see. cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") var stderr bytes.Buffer cmd.Stderr = &stderr out, err := cmd.Output() if err != nil { return nil, gitError(err, &stderr) } return out, nil } func gitError(err error, stderr *bytes.Buffer) error { if errors.Is(err, exec.ErrNotFound) { return errors.New("git is not installed; skills needs it to fetch sources (https://git-scm.com)") } msg := strings.TrimSpace(stderr.String()) switch { case msg == "": return err case strings.Contains(msg, "Repository not found"), strings.Contains(msg, "not found"), strings.Contains(msg, "could not read Username"), strings.Contains(msg, "Permission denied"): return fmt.Errorf("%s\n(check the URL, and that you can reach it with `git ls-remote`)", msg) } return errors.New(msg) }