Skip to content

Commit

Permalink
Git Error Handling Improvements + Git Error Resilient Analyze Local (#…
Browse files Browse the repository at this point in the history
…222)

* improving parsing of git errors to give more flexility in error handling
* git not found specific error
* adding interface type for all git errors
* wrapping errors for better context
* making the local git client resilient to git errors so poutine can be used on folders that are not in a git repo
* Made local git client resilient to git failures and to work when no git repos are present. Added handling to format the output data when no git repo exists
  • Loading branch information
SUSTAPLE117 authored Oct 24, 2024
1 parent e8f1c9f commit 160d529
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 27 deletions.
2 changes: 1 addition & 1 deletion analyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ func (a *Analyzer) generatePackageInsights(ctx context.Context, tempDir string,
}
err = pkg.NormalizePurl()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to normalize purl: %w", err)
}
return pkg, nil
}
Expand Down
2 changes: 1 addition & 1 deletion models/package_insights.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (p *PackageInsights) GetSourceGitRepoURI() string {
func (p *PackageInsights) NormalizePurl() error {
purl, err := NewPurl(p.Purl)
if err != nil {
return err
return fmt.Errorf("error creating new purl for normalization: %w", err)
}

p.Purl = purl.String()
Expand Down
137 changes: 121 additions & 16 deletions providers/gitops/gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ import (
"github.com/rs/zerolog/log"
)

type GitCloneError struct {
msg string
}

func (e *GitCloneError) Error() string {
return e.msg
}

type GitClient struct {
Command GitCommand
}
Expand All @@ -38,19 +30,100 @@ type GitCommand interface {
ReadFile(path string) ([]byte, error)
}

type GitError interface {
error
Command() string
}

type GitCommandError struct {
CommandStr string
Err error
}

func (e *GitCommandError) Error() string {
return fmt.Sprintf("error running command `%s`: %v", e.CommandStr, e.Err)
}

func (e *GitCommandError) Unwrap() error {
return e.Err
}

func (e *GitCommandError) Command() string {
return e.CommandStr
}

type GitExitError struct {
CommandStr string
Stderr string
ExitCode int
Err error
}

func (e *GitExitError) Error() string {
return fmt.Sprintf("command `%s` failed with exit code %d: %v, stderr: %s", e.CommandStr, e.ExitCode, e.Err, e.Stderr)
}

func (e *GitExitError) Unwrap() error {
return e.Err
}

func (e *GitExitError) Command() string {
return e.CommandStr
}

type GitNotFoundError struct {
CommandStr string
}

func (e *GitNotFoundError) Error() string {
return fmt.Sprintf("git binary not found for command `%s`. Please ensure Git is installed and available in your PATH.", e.CommandStr)
}

func (e *GitNotFoundError) Command() string {
return e.CommandStr
}

type ExecGitCommand struct{}

func (g *ExecGitCommand) Run(ctx context.Context, cmd string, args []string, dir string) ([]byte, error) {
command := exec.CommandContext(ctx, cmd, args...)
command.Dir = dir
stdout, err := command.Output()
var stdout, stderr strings.Builder
command.Stdout = &stdout
command.Stderr = &stderr

err := command.Run()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("command `%s` returned an error: %w stderr: %s", command.String(), err, string(bytes.TrimSpace(exitErr.Stderr)))
var execErr *exec.Error
if errors.As(err, &execErr) && errors.Is(execErr.Err, exec.ErrNotFound) {
return nil, &GitNotFoundError{
CommandStr: command.String(),
}
}

var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
exitCode := exitErr.ExitCode()
stderrMsg := strings.TrimSpace(stderr.String())

if stderrMsg == "" {
stderrMsg = exitErr.Error()
}

return nil, &GitExitError{
CommandStr: command.String(),
Stderr: stderrMsg,
ExitCode: exitCode,
Err: exitErr,
}
}
return nil, &GitCommandError{
CommandStr: command.String(),
Err: err,
}
return nil, fmt.Errorf("error running command: %w", err)
}
return stdout, nil

return []byte(stdout.String()), nil
}

func (g *ExecGitCommand) ReadFile(path string) ([]byte, error) {
Expand Down Expand Up @@ -160,15 +233,42 @@ type LocalGitClient struct {
}

func (g *LocalGitClient) GetRemoteOriginURL(ctx context.Context, repoPath string) (string, error) {
return g.GitClient.GetRemoteOriginURL(ctx, repoPath)
remoteOriginURL, err := g.GitClient.GetRemoteOriginURL(ctx, repoPath)
if err != nil {
var gitErr GitError
if errors.As(err, &gitErr) {
log.Debug().Err(err).Msg("failed to get remote origin URL for local repo")
return repoPath, nil
}
return "", err
}
return remoteOriginURL, nil
}

func (g *LocalGitClient) LastCommitDate(ctx context.Context, clonePath string) (time.Time, error) {
return g.GitClient.LastCommitDate(ctx, clonePath)
lastCommitDate, err := g.GitClient.LastCommitDate(ctx, clonePath)
if err != nil {
var gitErr GitError
if errors.As(err, &gitErr) {
log.Debug().Err(err).Msg("failed to get last commit date for local repo")
return time.Now(), nil
}
return time.Time{}, err
}
return lastCommitDate, nil
}

func (g *LocalGitClient) CommitSHA(clonePath string) (string, error) {
return g.GitClient.CommitSHA(clonePath)
commitSHA, err := g.GitClient.CommitSHA(clonePath)
if err != nil {
var gitErr GitError
if errors.As(err, &gitErr) {
log.Debug().Err(err).Msg("failed to get commit SHA for local repo")
return "", nil
}
return "", err
}
return commitSHA, nil
}

func (g *LocalGitClient) Clone(ctx context.Context, clonePath string, url string, token string, ref string) error {
Expand All @@ -182,6 +282,11 @@ func (g *LocalGitClient) GetRepoHeadBranchName(ctx context.Context, repoPath str

output, err := g.GitClient.Command.Run(ctx, cmd, args, repoPath)
if err != nil {
var gitErr GitError
if errors.As(err, &gitErr) {
log.Debug().Err(err).Msg("failed to get repo head branch name for local repo")
return "local", nil
}
return "", err
}

Expand Down
49 changes: 40 additions & 9 deletions providers/local/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ func (s *ScmClient) GetRepo(ctx context.Context, org string, name string) (analy
if err != nil {
return nil, err
}
baseUrl := s.GetProviderBaseURL()
baseUrl, err := s.GetBaseURL()
if err != nil {
var gitErr gitops.GitError
if errors.As(err, &gitErr) {
baseUrl = "localrepo"
}
}
return Repo{
BaseUrl: baseUrl,
Org: org,
Expand All @@ -45,39 +51,64 @@ func (s *ScmClient) GetToken() string {
return ""
}
func (s *ScmClient) GetProviderName() string {
return s.GetProviderBaseURL()
providerBaseURL, err := s.GetBaseURL()
if err != nil {
var gitErr gitops.GitError
if errors.As(err, &gitErr) {
return "provider"
}
return ""
}

return providerBaseURL
}
func (s *ScmClient) GetProviderVersion(ctx context.Context) (string, error) {
return "", nil
}
func (s *ScmClient) GetProviderBaseURL() string {
remote, err := s.gitClient.GetRemoteOriginURL(context.Background(), s.repoPath)
baseURL, err := s.GetBaseURL()
if err != nil {
log.Error().Err(err).Msg("failed to get remote url for repo")
var gitErr gitops.GitError
if errors.As(err, &gitErr) {
return s.repoPath
}
return ""
}
return baseURL
}

func (s *ScmClient) GetBaseURL() (string, error) {
remote, err := s.gitClient.GetRemoteOriginURL(context.Background(), s.repoPath)
if err != nil {
log.Debug().Err(err).Msg("failed to get remote url for local repo")
return "", err
}

if strings.HasPrefix(remote, "git@") {
return extractHostnameFromSSHURL(remote)
return extractHostnameFromSSHURL(remote), nil
}

parsedURL, err := url.Parse(remote)
if err != nil {
log.Error().Err(err).Msg("failed to parse remote url")
return ""
log.Error().Err(err).Msg("failed to parse remote url of local repo")
return "", err
}

if parsedURL.Hostname() == "" {
log.Error().Msg("repo remote url does not have a hostname")
return ""
return "", errors.New("repo remote url does not have a hostname")
}

return parsedURL.Hostname()
return parsedURL.Hostname(), nil
}

func (s *ScmClient) ParseRepoAndOrg(repoString string) (string, string, error) {
remoteURL, err := s.gitClient.GetRemoteOriginURL(context.Background(), s.repoPath)
if err != nil {
var gitErr gitops.GitError
if errors.As(err, &gitErr) {
return "", "local", nil
}
return "", "", err
}
if strings.Contains(remoteURL, "git@") {
Expand Down

0 comments on commit 160d529

Please sign in to comment.