2023-03-16 00:28:09 +03:00
|
|
|
// 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 (
|
2024-01-07 20:53:58 +03:00
|
|
|
"context"
|
2023-03-16 00:28:09 +03:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
2024-01-07 20:53:58 +03:00
|
|
|
"time"
|
2023-03-16 00:28:09 +03:00
|
|
|
|
|
|
|
"github.com/go-git/go-git/v5"
|
|
|
|
"github.com/go-git/go-git/v5/plumbing"
|
2024-01-07 20:53:58 +03:00
|
|
|
"github.com/go-git/go-git/v5/plumbing/object"
|
|
|
|
"github.com/go-git/go-git/v5/plumbing/storer"
|
2023-03-16 00:28:09 +03:00
|
|
|
cp "github.com/otiai10/copy"
|
|
|
|
|
2024-04-13 00:51:50 +03:00
|
|
|
"github.com/ossf/scorecard/v5/clients"
|
2023-03-16 00:28:09 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
const repoDir = "repo*"
|
|
|
|
|
|
|
|
var (
|
|
|
|
errNilCommitFound = errors.New("nil commit found")
|
|
|
|
errEmptyQuery = errors.New("query is empty")
|
2024-01-07 20:53:58 +03:00
|
|
|
errDefaultBranch = errors.New("default branch name could not be determined")
|
2024-03-05 04:37:50 +03:00
|
|
|
|
|
|
|
// ensure Client implements clients.RepoClient.
|
|
|
|
_ clients.RepoClient = (*Client)(nil)
|
2023-03-16 00:28:09 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
type Client struct {
|
2024-01-07 20:53:58 +03:00
|
|
|
repo clients.Repo
|
|
|
|
errListCommits error
|
2023-03-16 00:28:09 +03:00
|
|
|
gitRepo *git.Repository
|
|
|
|
worktree *git.Worktree
|
|
|
|
listCommits *sync.Once
|
|
|
|
tempDir string
|
|
|
|
commits []clients.Commit
|
|
|
|
commitDepth int
|
|
|
|
}
|
|
|
|
|
2024-01-07 20:53:58 +03:00
|
|
|
func (c *Client) InitRepo(repo clients.Repo, commitSHA string, commitDepth int) error {
|
2023-03-16 00:28:09 +03:00
|
|
|
// 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)
|
|
|
|
}
|
2024-01-07 20:53:58 +03:00
|
|
|
uri := repo.URI()
|
|
|
|
c.tempDir = tempDir
|
2023-03-16 00:28:09 +03:00
|
|
|
const filePrefix = "file://"
|
2024-01-07 20:53:58 +03:00
|
|
|
if strings.HasPrefix(uri, filePrefix) { //nolint:nestif
|
2023-03-16 00:28:09 +03:00
|
|
|
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 {
|
2024-01-07 20:53:58 +03:00
|
|
|
if !strings.HasPrefix(uri, "https://") && !strings.HasPrefix(uri, "ssh://") {
|
|
|
|
uri = "https://" + uri
|
|
|
|
}
|
|
|
|
if !strings.HasSuffix(uri, ".git") {
|
2024-03-26 01:56:22 +03:00
|
|
|
uri += ".git"
|
2024-01-07 20:53:58 +03:00
|
|
|
}
|
2023-03-16 00:28:09 +03:00
|
|
|
c.gitRepo, err = git.PlainClone(tempDir, false /*isBare*/, &git.CloneOptions{
|
|
|
|
URL: uri,
|
|
|
|
Progress: os.Stdout,
|
|
|
|
})
|
2024-01-07 20:53:58 +03:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("git.PlainClone: %w %s", err, uri)
|
2023-03-16 00:28:09 +03:00
|
|
|
}
|
|
|
|
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) {
|
2024-01-07 20:53:58 +03:00
|
|
|
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
|
2023-03-16 00:28:09 +03:00
|
|
|
}
|
|
|
|
|
2024-03-05 04:37:50 +03:00
|
|
|
func (c *Client) GetFileReader(filename string) (io.ReadCloser, error) {
|
2024-01-07 20:53:58 +03:00
|
|
|
// Create the full path of the file
|
|
|
|
fullPath := filepath.Join(c.tempDir, filename)
|
|
|
|
|
|
|
|
// Read the file
|
2024-03-05 04:37:50 +03:00
|
|
|
f, err := os.Open(fullPath)
|
2024-01-07 20:53:58 +03:00
|
|
|
if err != nil {
|
2024-03-05 04:37:50 +03:00
|
|
|
return nil, fmt.Errorf("os.Open: %w", err)
|
2024-01-07 20:53:58 +03:00
|
|
|
}
|
|
|
|
|
2024-03-05 04:37:50 +03:00
|
|
|
return f, nil
|
2024-01-07 20:53:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) IsArchived() (bool, error) {
|
|
|
|
return false, clients.ErrUnsupportedFeature
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) URI() string {
|
|
|
|
return c.repo.URI()
|
2023-03-16 00:28:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2024-01-07 20:53:58 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|