blob: 1205e5639089e2af541b2810a9f70c32824a17af [file] [log] [blame]
// Package git_checkout implements provider.Provider by shelling out to run git commands.
package git_checkout
import (
"bufio"
"bytes"
"context"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"go.opencensus.io/trace"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/git/git_common"
"go.skia.org/infra/go/gitauth"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/perf/go/config"
"go.skia.org/infra/perf/go/git/provider"
"go.skia.org/infra/perf/go/types"
"golang.org/x/oauth2/google"
)
// Impl implements provider.Provider.
type Impl struct {
// gitFullPath is the path of the git executable.
gitFullPath string
// repoFullPath if the full path of the checked out Git repo.
repoFullPath string
// startCommit is the commit in the repo where we start tracking commits. If
// not supplied then we start with the first commit in the repo as reachable
// from HEAD.
startCommit string
}
// New returns a new instance of Impl, which implements provider.Provider.
func New(ctx context.Context, instanceConfig *config.InstanceConfig) (*Impl, error) {
// Do git authentication if required.
if instanceConfig.GitRepoConfig.GitAuthType == config.GitAuthGerrit {
sklog.Info("Authenticating to Gerrit.")
ts, err := google.DefaultTokenSource(ctx, auth.ScopeGerrit)
if err != nil {
return nil, skerr.Wrapf(err, "Failed to get tokensource perfgit.Git for config %v", *instanceConfig)
}
if _, err := gitauth.New(ts, "/tmp/git-cookie", true, ""); err != nil {
return nil, skerr.Wrapf(err, "Failed to gitauth perfgit.Git for config %v", *instanceConfig)
}
}
// Find the path to the git executable, which might be relative to working dir.
gitFullPath, _, _, err := git_common.FindGit(ctx)
if err != nil {
return nil, skerr.Wrapf(err, "Failed to find git.")
}
// Force the path to be absolute.
gitFullPath, err = filepath.Abs(gitFullPath)
if err != nil {
return nil, skerr.Wrapf(err, "Failed to get absolute path to git.")
}
// Clone the git repo if necessary.
sklog.Infof("Cloning repo.")
if _, err := os.Stat(instanceConfig.GitRepoConfig.Dir); os.IsNotExist(err) {
cmd := exec.CommandContext(ctx, gitFullPath, "clone", instanceConfig.GitRepoConfig.URL, instanceConfig.GitRepoConfig.Dir)
if err := cmd.Run(); err != nil {
exerr := err.(*exec.ExitError)
return nil, skerr.Wrapf(err, "Failed to clone repo: %s - %s", err, exerr.Stderr)
}
}
return &Impl{
gitFullPath: gitFullPath,
repoFullPath: instanceConfig.GitRepoConfig.Dir,
startCommit: instanceConfig.GitRepoConfig.StartCommit,
}, nil
}
// CommitsFromMostRecentGitHashToHead implements provider.Provider.
func (i Impl) CommitsFromMostRecentGitHashToHead(ctx context.Context, mostRecentGitHash string, cb provider.CommitProcessor) error {
var cmd *exec.Cmd
if mostRecentGitHash == "" {
mostRecentGitHash = i.startCommit
}
if mostRecentGitHash == "" {
cmd = exec.CommandContext(ctx, i.gitFullPath, "rev-list", "HEAD", `--pretty=%aN <%aE>%n%s%n%ct`, "--reverse")
} else {
// Add all the commits from the repo since the last time we looked.
cmd = exec.CommandContext(ctx, i.gitFullPath, "rev-list", "HEAD", "^"+mostRecentGitHash, `--pretty=%aN <%aE>%n%s%n%ct`, "--reverse")
}
cmd.Dir = i.repoFullPath
stdout, err := cmd.StdoutPipe()
if err != nil {
return skerr.Wrap(err)
}
if err := cmd.Start(); err != nil {
return skerr.Wrap(err)
}
err = parseGitRevLogStream(stdout, func(p provider.Commit) error {
return cb(p)
})
if err != nil {
// Once we've successfully called cmd.Start() we must always call
// cmd.Wait() to close stdout.
_ = cmd.Wait()
return skerr.Wrap(err)
}
return nil
}
// GitHashesInRangeForFile implements provider.Provider.
func (i Impl) GitHashesInRangeForFile(ctx context.Context, begin, end, filename string) ([]string, error) {
var revisionRange string
if begin == "" {
begin = i.startCommit
}
if begin == "" {
// git log revision range queries of the form hash1..hash2 are exclusive
// of hash1, so we need to always back up begin one commit, except in
// the case where the commit number is 0, then we change the revision
// range.
revisionRange = end
} else {
revisionRange = begin + ".." + end
}
// Build the git log command to run.
cmd := exec.CommandContext(ctx, i.gitFullPath, "log", revisionRange, "--reverse", "--format=format:%H", "--", filename)
cmd.Dir = i.repoFullPath
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, skerr.Wrap(err)
}
if err := cmd.Start(); err != nil {
return nil, skerr.Wrap(err)
}
// Read the git log output.
scanner := bufio.NewScanner(stdout)
ret := []string{}
for scanner.Scan() {
ret = append(ret, scanner.Text())
}
if scanner.Err() != nil {
// Once we've successfully called cmd.Start() we must always call
// cmd.Wait() to close stdout.
_ = cmd.Wait()
return nil, skerr.Wrap(err)
}
if err := cmd.Wait(); err != nil {
exerr := err.(*exec.ExitError)
return nil, skerr.Wrapf(err, "Failed to get logs: %s", exerr.Stderr)
}
return ret, nil
}
// LogEntry implements provider.Provider.
func (i Impl) LogEntry(ctx context.Context, hash string) (string, error) {
// Build the git log command to run.
cmd := exec.CommandContext(ctx, i.gitFullPath, "show", "-s", hash)
cmd.Dir = i.repoFullPath
var out bytes.Buffer
cmd.Stdout = &out
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", skerr.Wrapf(err, "Failed running %q: stdout: %q stderr: %q", cmd.String(), out.String(), stderr.String())
}
return out.String(), nil
}
type parseGitRevLogStreamProcessSingleCommit func(commit provider.Commit) error
// parseGitRevLogStream parses the input stream for input of the form:
//
// commit 6079a7810530025d9877916895dd14eb8bb454c0
// Joe Gregorio <joe@bitworking.org>
// Change #9
// 1584837783
// commit 977e0ef44bec17659faf8c5d4025c5a068354817
// Joe Gregorio <joe@bitworking.org>
// Change #8
// 1584837783
//
// And calls the parseGitRevLogStreamProcessSingleCommit function with each
// entry it finds. The passed in Commit has all valid fields except
// CommitNumber, which is set to types.BadCommitNumber.
func parseGitRevLogStream(r io.ReadCloser, f parseGitRevLogStreamProcessSingleCommit) error {
scanner := bufio.NewScanner(r)
lineNumber := 0
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "commit ") {
return skerr.Fmt("Invalid format, expected commit at line %d: %q", lineNumber, line)
}
lineNumber++
gitHash := strings.Split(line, " ")[1]
if !scanner.Scan() {
return skerr.Fmt("Ran out of input, expecting an author line: %d", lineNumber)
}
lineNumber++
author := scanner.Text()
if !scanner.Scan() {
return skerr.Fmt("Ran out of input, expecting a subject line: %d", lineNumber)
}
lineNumber++
subject := scanner.Text()
if !scanner.Scan() {
return skerr.Fmt("Ran out of input, expecting a timestamp line: %d", lineNumber)
}
lineNumber++
timestampString := scanner.Text()
ts, err := strconv.ParseInt(timestampString, 10, 64)
if err != nil {
return skerr.Fmt("Failed to parse timestamp %q at line %d", timestampString, lineNumber)
}
if err := f(provider.Commit{
CommitNumber: types.BadCommitNumber,
GitHash: gitHash,
Timestamp: ts,
Author: author,
Subject: subject}); err != nil {
return skerr.Wrap(err)
}
}
return skerr.Wrap(scanner.Err())
}
// Update implements provider.Provider.
func (i Impl) Update(ctx context.Context) error {
ctx, span := trace.StartSpan(ctx, "perfgit.pull")
defer span.End()
cmd := exec.CommandContext(ctx, i.gitFullPath, "pull")
cmd.Dir = i.repoFullPath
if err := cmd.Run(); err != nil {
exerr := err.(*exec.ExitError)
return skerr.Wrapf(err, "Failed to pull repo %q with git %q: %s", i.repoFullPath, i.gitFullPath, exerr.Stderr)
}
return nil
}
var _ provider.Provider = Impl{}