scorecard/clients/git/client.go

384 lines
9.8 KiB
Go
Raw Permalink Normal View History

// Copyright 2021 OpenSSF Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package git defines helper functions for clients.RepoClient interface.
package git
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/storer"
cp "github.com/otiai10/copy"
"github.com/ossf/scorecard/v5/clients"
)
const repoDir = "repo*"
var (
errNilCommitFound = errors.New("nil commit found")
errEmptyQuery = errors.New("query is empty")
errDefaultBranch = errors.New("default branch name could not be determined")
// ensure Client implements clients.RepoClient.
_ clients.RepoClient = (*Client)(nil)
)
type Client struct {
repo clients.Repo
errListCommits error
gitRepo *git.Repository
worktree *git.Worktree
listCommits *sync.Once
tempDir string
commits []clients.Commit
commitDepth int
}
func (c *Client) InitRepo(repo clients.Repo, commitSHA string, commitDepth int) error {
// cleanup previous state, if any.
c.Close()
c.listCommits = new(sync.Once)
c.commits = nil
// init
c.commitDepth = commitDepth
tempDir, err := os.MkdirTemp("", repoDir)
if err != nil {
return fmt.Errorf("os.MkdirTemp: %w", err)
}
uri := repo.URI()
c.tempDir = tempDir
const filePrefix = "file://"
if strings.HasPrefix(uri, filePrefix) { //nolint:nestif
if err := cp.Copy(strings.TrimPrefix(uri, filePrefix), tempDir); err != nil {
return fmt.Errorf("cp.Copy: %w", err)
}
c.gitRepo, err = git.PlainOpen(tempDir)
if err != nil {
return fmt.Errorf("git.PlainOpen: %w", err)
}
} else {
if !strings.HasPrefix(uri, "https://") && !strings.HasPrefix(uri, "ssh://") {
uri = "https://" + uri
}
if !strings.HasSuffix(uri, ".git") {
:seedling: Bump github.com/golangci/golangci-lint from 1.56.2 to 1.57.1 in /tools (#3966) * :seedling: Bump github.com/golangci/golangci-lint in /tools Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.56.2 to 1.57.1. - [Release notes](https://github.com/golangci/golangci-lint/releases) - [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md) - [Commits](https://github.com/golangci/golangci-lint/compare/v1.56.2...v1.57.1) --- updated-dependencies: - dependency-name: github.com/golangci/golangci-lint dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * remove unused wrapcheck nolint directives wrapcheck v2.8.3 includes a fix for false positives in func literals. Signed-off-by: Spencer Schrock <sschrock@google.com> * satisfy assignOp gocritic linter Signed-off-by: Spencer Schrock <sschrock@google.com> * convert const regex to MustCompile included at package level to ensure regex compiles at build time. These could stay as func regexes if desired, but we'd need test coverage for the piper code so we know we wont panic Signed-off-by: Spencer Schrock <sschrock@google.com> * satisfy unslice linter Signed-off-by: Spencer Schrock <sschrock@google.com> * satisfy wrapperFunc linter This seems like a nice readability change anyway Signed-off-by: Spencer Schrock <sschrock@google.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Spencer Schrock <sschrock@google.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Spencer Schrock <sschrock@google.com>
2024-03-26 01:56:22 +03:00
uri += ".git"
}
c.gitRepo, err = git.PlainClone(tempDir, false /*isBare*/, &git.CloneOptions{
URL: uri,
Progress: os.Stdout,
})
}
if err != nil {
return fmt.Errorf("git.PlainClone: %w %s", err, uri)
}
c.tempDir = tempDir
c.worktree, err = c.gitRepo.Worktree()
if err != nil {
return fmt.Errorf("git.Worktree: %w", err)
}
// git checkout
if commitSHA != clients.HeadSHA {
if err := c.worktree.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(commitSHA),
Force: true, // throw away any unsaved changes.
}); err != nil {
return fmt.Errorf("git.Worktree: %w", err)
}
}
return nil
}
func (c *Client) ListCommits() ([]clients.Commit, error) {
c.listCommits.Do(func() {
commitIter, err := c.gitRepo.Log(&git.LogOptions{
Order: git.LogOrderCommitterTime,
})
if err != nil {
c.errListCommits = fmt.Errorf("git.CommitObjects: %w", err)
return
}
c.commits = make([]clients.Commit, 0, c.commitDepth)
for i := 0; i < c.commitDepth; i++ {
commit, err := commitIter.Next()
if err != nil && !errors.Is(err, io.EOF) {
c.errListCommits = fmt.Errorf("commitIter.Next: %w", err)
return
}
// No more commits.
if errors.Is(err, io.EOF) {
break
}
if commit == nil {
// Not sure in what case a nil commit is returned. Fail explicitly.
c.errListCommits = fmt.Errorf("%w", errNilCommitFound)
return
}
c.commits = append(c.commits, clients.Commit{
SHA: commit.Hash.String(),
Message: commit.Message,
CommittedDate: commit.Committer.When,
Committer: clients.User{
Login: commit.Committer.Email,
},
})
}
})
return c.commits, c.errListCommits
}
func (c *Client) Search(request clients.SearchRequest) (clients.SearchResponse, error) {
// Pattern
if request.Query == "" {
return clients.SearchResponse{}, errEmptyQuery
}
queryRegexp, err := regexp.Compile(request.Query)
if err != nil {
return clients.SearchResponse{}, fmt.Errorf("regexp.Compile: %w", err)
}
grepOpts := &git.GrepOptions{
Patterns: []*regexp.Regexp{queryRegexp},
}
// path/filename
var pathExpr string
switch {
case request.Path != "" && request.Filename != "":
pathExpr = filepath.Join(fmt.Sprintf("^%s", request.Path),
fmt.Sprintf(".*%s$", request.Filename))
case request.Path != "":
pathExpr = fmt.Sprintf("^%s", request.Path)
case request.Filename != "":
pathExpr = filepath.Join(".*", fmt.Sprintf("%s$", request.Filename))
}
if pathExpr != "" {
pathRegexp, err := regexp.Compile(pathExpr)
if err != nil {
return clients.SearchResponse{}, fmt.Errorf("regexp.Compile: %w", err)
}
grepOpts.PathSpecs = append(grepOpts.PathSpecs, pathRegexp)
}
// Grep
grepResults, err := c.worktree.Grep(grepOpts)
if err != nil {
return clients.SearchResponse{}, fmt.Errorf("git.Grep: %w", err)
}
ret := clients.SearchResponse{}
for _, grepResult := range grepResults {
ret.Results = append(ret.Results, clients.SearchResult{
Path: grepResult.FileName,
})
}
ret.Hits = len(grepResults)
return ret, nil
}
func (c *Client) ListFiles(predicate func(string) (bool, error)) ([]string, error) {
ref, err := c.gitRepo.Head()
if err != nil {
return nil, fmt.Errorf("git.Head: %w", err)
}
commit, err := c.gitRepo.CommitObject(ref.Hash())
if err != nil {
return nil, fmt.Errorf("git.CommitObject: %w", err)
}
tree, err := commit.Tree()
if err != nil {
return nil, fmt.Errorf("git.Commit.Tree: %w", err)
}
var files []string
err = tree.Files().ForEach(func(f *object.File) error {
shouldInclude, err := predicate(f.Name)
if err != nil {
return fmt.Errorf("error applying predicate to file %s: %w", f.Name, err)
}
if shouldInclude {
files = append(files, f.Name)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("git.Tree.Files: %w", err)
}
return files, nil
}
func (c *Client) GetFileReader(filename string) (io.ReadCloser, error) {
// Create the full path of the file
fullPath := filepath.Join(c.tempDir, filename)
// Read the file
f, err := os.Open(fullPath)
if err != nil {
return nil, fmt.Errorf("os.Open: %w", err)
}
return f, nil
}
func (c *Client) IsArchived() (bool, error) {
return false, clients.ErrUnsupportedFeature
}
func (c *Client) URI() string {
return c.repo.URI()
}
func (c *Client) Close() error {
if err := os.RemoveAll(c.tempDir); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("os.RemoveAll: %w", err)
}
return nil
}
func (c *Client) GetBranch(branch string) (*clients.BranchRef, error) {
// Get the branch reference
ref, err := c.gitRepo.Branch(branch)
if err != nil {
return nil, fmt.Errorf("git.Branch: %w", err)
}
// Get the commit object
if err != nil {
return nil, fmt.Errorf("git.CommitObject: %w", err)
}
f := false
// Create the BranchRef object
branchRef := &clients.BranchRef{
Name: &ref.Name,
Protected: &f,
}
return branchRef, nil
}
func (c *Client) GetCreatedAt() (time.Time, error) {
// Retrieve the first commit of the repository
commitIter, err := c.gitRepo.Log(&git.LogOptions{Order: git.LogOrderCommitterTime})
if err != nil {
return time.Time{}, fmt.Errorf("git.Log: %w", err)
}
defer commitIter.Close()
// Iterate through the commits to find the first one
var firstCommit *object.Commit
err = commitIter.ForEach(func(c *object.Commit) error {
firstCommit = c
return storer.ErrStop
})
if err != nil && !errors.Is(err, storer.ErrStop) {
return time.Time{}, fmt.Errorf("commitIter.ForEach: %w", err)
}
if firstCommit == nil {
return time.Time{}, errNilCommitFound
}
// Return the commit time of the first commit
return firstCommit.Committer.When, nil
}
func (c *Client) GetDefaultBranchName() (string, error) {
headRef, err := c.gitRepo.Head()
if err != nil {
return "", fmt.Errorf("git.Head: %w", err)
}
// Extract the branch name from the Head reference
defaultBranch := headRef.Name()
if defaultBranch == "" {
return "", errDefaultBranch
}
return string(defaultBranch), nil
}
func (c *Client) GetDefaultBranch() (*clients.BranchRef, error) {
// TODO: Implement this
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) GetOrgRepoClient(ctx context.Context) (clients.RepoClient, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) ListIssues() ([]clients.Issue, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) ListLicenses() ([]clients.License, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) ListReleases() ([]clients.Release, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) ListContributors() ([]clients.User, error) {
// TODO: Implement this
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) ListCheckRunsForRef(ref string) ([]clients.CheckRun, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) ListStatuses(ref string) ([]clients.Status, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) ListWebhooks() ([]clients.Webhook, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) ListProgrammingLanguages() ([]clients.Language, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) SearchCommits(request clients.SearchCommitsOptions) ([]clients.Commit, error) {
return nil, clients.ErrUnsupportedFeature
}
func (c *Client) LocalPath() (string, error) {
return c.tempDir, nil
}