blob: 9bfcda80de94d8e99a1df90f844af404bfad3c61 [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package stdlib
import (
"bytes"
"context"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/version"
)
// A goRepo represents a git repo holding the Go standard library.
type goRepo interface {
// Clone the repo at the given version to the directory.
clone(ctx context.Context, version string, toDirectory string) (hash string, err error)
// Return all the refs of the repo.
refs(ctx context.Context) ([]ref, error)
}
type remoteGoRepo struct{}
func (remoteGoRepo) clone(ctx context.Context, v, directory string) (hash string, err error) {
defer derrors.Wrap(&err, "remoteGoRepo.clone(%q)", v)
refName, err := refNameForVersion(v)
if err != nil {
return "", err
}
if err := os.MkdirAll(directory, 0777); err != nil {
return "", err
}
cmd := exec.CommandContext(ctx, "git", "init")
cmd.Dir = directory
if err := cmd.Run(); err != nil {
return "", err
}
cmd = exec.CommandContext(ctx, "git", "fetch", "-f", "--depth=1", "--", GoRepoURL, refName)
cmd.Dir = directory
if b, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("running git fetch: %v: %s", err, b)
}
cmd = exec.CommandContext(ctx, "git", "rev-parse", "FETCH_HEAD")
cmd.Dir = directory
b, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("running git rev-parse: %v: %s", err, ee.Stderr)
}
return "", fmt.Errorf("running git rev-parse: %v", err)
}
cmd = exec.CommandContext(ctx, "git", "checkout", "FETCH_HEAD")
cmd.Dir = directory
if err := cmd.Run(); err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("running git checkout: %v: %s", err, ee.Stderr)
}
return "", fmt.Errorf("running git checkout: %v", err)
}
return strings.TrimSpace(string(b)), nil
}
type ref struct {
hash string
name string
}
func (remoteGoRepo) refs(ctx context.Context) ([]ref, error) {
cmd := exec.CommandContext(ctx, "git", "ls-remote", "--", GoRepoURL)
b, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("running git ls-remote: %v: %s", err, ee.Stderr)
}
return nil, fmt.Errorf("running git ls-remote: %v", err)
}
return gitOutputToRefs(b)
}
func gitOutputToRefs(b []byte) ([]ref, error) {
var refs []ref
b = bytes.TrimSpace(b)
for _, line := range bytes.Split(b, []byte("\n")) {
fields := bytes.Fields(line)
if len(fields) != 2 {
return nil, fmt.Errorf("invalid line in output from git ls-remote: %q: should have two fields", line)
}
refs = append(refs, ref{hash: string(fields[0]), name: string(fields[1])})
}
return refs, nil
}
type localGoRepo struct {
path string
}
func newLocalGoRepo(path string) *localGoRepo {
return &localGoRepo{
path: path,
}
}
func (g *localGoRepo) refs(ctx context.Context) (refs []ref, err error) {
defer derrors.Wrap(&err, "localGoRepo(%s).refs", g.path)
cmd := exec.CommandContext(ctx, "git", "show-ref")
cmd.Dir = g.path
b, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("running git show-ref: %s", ee.Stderr)
}
return nil, fmt.Errorf("running git show-ref: %v", err)
}
return gitOutputToRefs(b)
}
func (g *localGoRepo) clone(ctx context.Context, v, directory string) (hash string, err error) {
return "", nil
}
type testGoRepo struct {
}
func (t *testGoRepo) clone(ctx context.Context, v, directory string) (hash string, err error) {
defer derrors.Wrap(&err, "testGoRepo.clone(%q)", v)
if v == TestMasterVersion {
v = version.Master
}
cmd := exec.CommandContext(ctx, "git", "init")
cmd.Dir = directory
if err := cmd.Run(); err != nil {
return "", err
}
testdatadir := filepath.Join(testDataPath("testdata"), v)
err = filepath.Walk(testdatadir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(testdatadir, path)
if err != nil {
return err
}
dstpath := filepath.Join(directory, rel)
if info.Mode().IsDir() {
os.MkdirAll(dstpath, 0777)
return nil
}
b, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("reading %q: %v", path, err)
}
os.WriteFile(dstpath, b, 0666)
cmd := exec.CommandContext(ctx, "git", "add", "--", dstpath)
cmd.Dir = directory
if err := cmd.Run(); err != nil {
return fmt.Errorf("running git add: %v", err)
}
return nil
})
if err != nil {
return "", err
}
cmd = exec.CommandContext(ctx, "git", "commit", "--allow-empty-message", "--author=Joe Random <joe@example.com>",
"--message=")
cmd.Dir = directory
commitTime := fmt.Sprintf("%v +0000", TestCommitTime.Unix())
name := "Joe Random"
email := "joe@example.com"
cmd.Env = append(cmd.Environ(), []string{
"GIT_COMMITTER_NAME=" + name, "GIT_AUTHOR_NAME=" + name,
"GIT_COMMITTER_EMAIL=" + email, "GIT_AUTHOR_EMAIL=" + email,
"GIT_COMMITTER_DATE=" + commitTime, "GIT_AUTHOR_DATE=" + commitTime}...)
if err := cmd.Run(); err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("running git commit: %v: %s", err, ee.Stderr)
}
return "", fmt.Errorf("running git commit: %v", err)
}
cmd = exec.CommandContext(ctx, "git", "rev-parse", "HEAD")
cmd.Dir = directory
b, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("running git rev-parse: %v: %s", err, ee.Stderr)
}
return "", fmt.Errorf("running git rev-parse: %v", err)
}
return strings.TrimSpace(string(b)), nil
}
// testDataPath returns a path corresponding to a path relative to the calling
// test file. For convenience, rel is assumed to be "/"-delimited.
// It is a copy of testhelper.TestDataPath, which we can't use in this
// file because it is supposed to only be depended on by test files.
//
// It panics on failure.
func testDataPath(rel string) string {
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic("unable to determine relative path")
}
return filepath.Clean(filepath.Join(filepath.Dir(filename), filepath.FromSlash(rel)))
}
// References used for Versions during testing.
var testRefs = []string{
// stdlib versions
"refs/tags/go1.2.1",
"refs/tags/go1.3.2",
"refs/tags/go1.4.2",
"refs/tags/go1.4.3",
"refs/tags/go1.6",
"refs/tags/go1.6.3",
"refs/tags/go1.6beta1",
"refs/tags/go1.8",
"refs/tags/go1.8rc2",
"refs/tags/go1.9rc1",
"refs/tags/go1.11",
"refs/tags/go1.12",
"refs/tags/go1.12.1",
"refs/tags/go1.12.5",
"refs/tags/go1.12.9",
"refs/tags/go1.13",
"refs/tags/go1.13beta1",
"refs/tags/go1.14.6",
"refs/tags/go1.21.0",
"refs/heads/dev.fuzz",
"refs/heads/master",
// other tags
"refs/changes/56/93156/13",
"refs/tags/release.r59",
"refs/tags/weekly.2011-04-13",
}
func (t *testGoRepo) refs(ctx context.Context) ([]ref, error) {
var rs []ref
for _, r := range testRefs {
// Only the name is ever used, so the referent can be empty.
rs = append(rs, ref{name: r})
}
return rs, nil
}