Skip to content

Commit 160d529

Browse files
authored
Git Error Handling Improvements + Git Error Resilient Analyze Local (#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
1 parent e8f1c9f commit 160d529

File tree

4 files changed

+163
-27
lines changed

4 files changed

+163
-27
lines changed

analyze/analyze.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func (a *Analyzer) generatePackageInsights(ctx context.Context, tempDir string,
359359
}
360360
err = pkg.NormalizePurl()
361361
if err != nil {
362-
return nil, err
362+
return nil, fmt.Errorf("failed to normalize purl: %w", err)
363363
}
364364
return pkg, nil
365365
}

models/package_insights.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (p *PackageInsights) GetSourceGitRepoURI() string {
6363
func (p *PackageInsights) NormalizePurl() error {
6464
purl, err := NewPurl(p.Purl)
6565
if err != nil {
66-
return err
66+
return fmt.Errorf("error creating new purl for normalization: %w", err)
6767
}
6868

6969
p.Purl = purl.String()

providers/gitops/gitops.go

Lines changed: 121 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,6 @@ import (
1414
"github.com/rs/zerolog/log"
1515
)
1616

17-
type GitCloneError struct {
18-
msg string
19-
}
20-
21-
func (e *GitCloneError) Error() string {
22-
return e.msg
23-
}
24-
2517
type GitClient struct {
2618
Command GitCommand
2719
}
@@ -38,19 +30,100 @@ type GitCommand interface {
3830
ReadFile(path string) ([]byte, error)
3931
}
4032

33+
type GitError interface {
34+
error
35+
Command() string
36+
}
37+
38+
type GitCommandError struct {
39+
CommandStr string
40+
Err error
41+
}
42+
43+
func (e *GitCommandError) Error() string {
44+
return fmt.Sprintf("error running command `%s`: %v", e.CommandStr, e.Err)
45+
}
46+
47+
func (e *GitCommandError) Unwrap() error {
48+
return e.Err
49+
}
50+
51+
func (e *GitCommandError) Command() string {
52+
return e.CommandStr
53+
}
54+
55+
type GitExitError struct {
56+
CommandStr string
57+
Stderr string
58+
ExitCode int
59+
Err error
60+
}
61+
62+
func (e *GitExitError) Error() string {
63+
return fmt.Sprintf("command `%s` failed with exit code %d: %v, stderr: %s", e.CommandStr, e.ExitCode, e.Err, e.Stderr)
64+
}
65+
66+
func (e *GitExitError) Unwrap() error {
67+
return e.Err
68+
}
69+
70+
func (e *GitExitError) Command() string {
71+
return e.CommandStr
72+
}
73+
74+
type GitNotFoundError struct {
75+
CommandStr string
76+
}
77+
78+
func (e *GitNotFoundError) Error() string {
79+
return fmt.Sprintf("git binary not found for command `%s`. Please ensure Git is installed and available in your PATH.", e.CommandStr)
80+
}
81+
82+
func (e *GitNotFoundError) Command() string {
83+
return e.CommandStr
84+
}
85+
4186
type ExecGitCommand struct{}
4287

4388
func (g *ExecGitCommand) Run(ctx context.Context, cmd string, args []string, dir string) ([]byte, error) {
4489
command := exec.CommandContext(ctx, cmd, args...)
4590
command.Dir = dir
46-
stdout, err := command.Output()
91+
var stdout, stderr strings.Builder
92+
command.Stdout = &stdout
93+
command.Stderr = &stderr
94+
95+
err := command.Run()
4796
if err != nil {
48-
if exitErr, ok := err.(*exec.ExitError); ok {
49-
return nil, fmt.Errorf("command `%s` returned an error: %w stderr: %s", command.String(), err, string(bytes.TrimSpace(exitErr.Stderr)))
97+
var execErr *exec.Error
98+
if errors.As(err, &execErr) && errors.Is(execErr.Err, exec.ErrNotFound) {
99+
return nil, &GitNotFoundError{
100+
CommandStr: command.String(),
101+
}
102+
}
103+
104+
var exitErr *exec.ExitError
105+
if errors.As(err, &exitErr) {
106+
exitCode := exitErr.ExitCode()
107+
stderrMsg := strings.TrimSpace(stderr.String())
108+
109+
if stderrMsg == "" {
110+
stderrMsg = exitErr.Error()
111+
}
112+
113+
return nil, &GitExitError{
114+
CommandStr: command.String(),
115+
Stderr: stderrMsg,
116+
ExitCode: exitCode,
117+
Err: exitErr,
118+
}
119+
}
120+
return nil, &GitCommandError{
121+
CommandStr: command.String(),
122+
Err: err,
50123
}
51-
return nil, fmt.Errorf("error running command: %w", err)
52124
}
53-
return stdout, nil
125+
126+
return []byte(stdout.String()), nil
54127
}
55128

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

162235
func (g *LocalGitClient) GetRemoteOriginURL(ctx context.Context, repoPath string) (string, error) {
163-
return g.GitClient.GetRemoteOriginURL(ctx, repoPath)
236+
remoteOriginURL, err := g.GitClient.GetRemoteOriginURL(ctx, repoPath)
237+
if err != nil {
238+
var gitErr GitError
239+
if errors.As(err, &gitErr) {
240+
log.Debug().Err(err).Msg("failed to get remote origin URL for local repo")
241+
return repoPath, nil
242+
}
243+
return "", err
244+
}
245+
return remoteOriginURL, nil
164246
}
165247

166248
func (g *LocalGitClient) LastCommitDate(ctx context.Context, clonePath string) (time.Time, error) {
167-
return g.GitClient.LastCommitDate(ctx, clonePath)
249+
lastCommitDate, err := g.GitClient.LastCommitDate(ctx, clonePath)
250+
if err != nil {
251+
var gitErr GitError
252+
if errors.As(err, &gitErr) {
253+
log.Debug().Err(err).Msg("failed to get last commit date for local repo")
254+
return time.Now(), nil
255+
}
256+
return time.Time{}, err
257+
}
258+
return lastCommitDate, nil
168259
}
169260

170261
func (g *LocalGitClient) CommitSHA(clonePath string) (string, error) {
171-
return g.GitClient.CommitSHA(clonePath)
262+
commitSHA, err := g.GitClient.CommitSHA(clonePath)
263+
if err != nil {
264+
var gitErr GitError
265+
if errors.As(err, &gitErr) {
266+
log.Debug().Err(err).Msg("failed to get commit SHA for local repo")
267+
return "", nil
268+
}
269+
return "", err
270+
}
271+
return commitSHA, nil
172272
}
173273

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

183283
output, err := g.GitClient.Command.Run(ctx, cmd, args, repoPath)
184284
if err != nil {
285+
var gitErr GitError
286+
if errors.As(err, &gitErr) {
287+
log.Debug().Err(err).Msg("failed to get repo head branch name for local repo")
288+
return "local", nil
289+
}
185290
return "", err
186291
}
187292

providers/local/client.go

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ func (s *ScmClient) GetRepo(ctx context.Context, org string, name string) (analy
3434
if err != nil {
3535
return nil, err
3636
}
37-
baseUrl := s.GetProviderBaseURL()
37+
baseUrl, err := s.GetBaseURL()
38+
if err != nil {
39+
var gitErr gitops.GitError
40+
if errors.As(err, &gitErr) {
41+
baseUrl = "localrepo"
42+
}
43+
}
3844
return Repo{
3945
BaseUrl: baseUrl,
4046
Org: org,
@@ -45,39 +51,64 @@ func (s *ScmClient) GetToken() string {
4551
return ""
4652
}
4753
func (s *ScmClient) GetProviderName() string {
48-
return s.GetProviderBaseURL()
54+
providerBaseURL, err := s.GetBaseURL()
55+
if err != nil {
56+
var gitErr gitops.GitError
57+
if errors.As(err, &gitErr) {
58+
return "provider"
59+
}
60+
return ""
61+
}
62+
63+
return providerBaseURL
4964
}
5065
func (s *ScmClient) GetProviderVersion(ctx context.Context) (string, error) {
5166
return "", nil
5267
}
5368
func (s *ScmClient) GetProviderBaseURL() string {
54-
remote, err := s.gitClient.GetRemoteOriginURL(context.Background(), s.repoPath)
69+
baseURL, err := s.GetBaseURL()
5570
if err != nil {
56-
log.Error().Err(err).Msg("failed to get remote url for repo")
71+
var gitErr gitops.GitError
72+
if errors.As(err, &gitErr) {
73+
return s.repoPath
74+
}
5775
return ""
5876
}
77+
return baseURL
78+
}
79+
80+
func (s *ScmClient) GetBaseURL() (string, error) {
81+
remote, err := s.gitClient.GetRemoteOriginURL(context.Background(), s.repoPath)
82+
if err != nil {
83+
log.Debug().Err(err).Msg("failed to get remote url for local repo")
84+
return "", err
85+
}
5986

6087
if strings.HasPrefix(remote, "git@") {
61-
return extractHostnameFromSSHURL(remote)
88+
return extractHostnameFromSSHURL(remote), nil
6289
}
6390

6491
parsedURL, err := url.Parse(remote)
6592
if err != nil {
66-
log.Error().Err(err).Msg("failed to parse remote url")
67-
return ""
93+
log.Error().Err(err).Msg("failed to parse remote url of local repo")
94+
return "", err
6895
}
6996

7097
if parsedURL.Hostname() == "" {
7198
log.Error().Msg("repo remote url does not have a hostname")
72-
return ""
99+
return "", errors.New("repo remote url does not have a hostname")
73100
}
74101

75-
return parsedURL.Hostname()
102+
return parsedURL.Hostname(), nil
76103
}
77104

78105
func (s *ScmClient) ParseRepoAndOrg(repoString string) (string, string, error) {
79106
remoteURL, err := s.gitClient.GetRemoteOriginURL(context.Background(), s.repoPath)
80107
if err != nil {
108+
var gitErr gitops.GitError
109+
if errors.As(err, &gitErr) {
110+
return "", "local", nil
111+
}
81112
return "", "", err
82113
}
83114
if strings.Contains(remoteURL, "git@") {

0 commit comments

Comments
 (0)