github.com/snyk/vervet/v5@v5.11.1-0.20240202085829-ad4dd7fb6101/internal/linter/optic/git.go (about)

     1  package optic
     2  
     3  import (
     4  	"io"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/bmatcuk/doublestar/v4"
    10  	"github.com/go-git/go-git/v5"
    11  	"github.com/go-git/go-git/v5/plumbing"
    12  	"github.com/go-git/go-git/v5/plumbing/object"
    13  	"go.uber.org/multierr"
    14  
    15  	"github.com/snyk/vervet/v5/config"
    16  )
    17  
    18  // gitRepoSource is a fileSource that resolves files out of a specific git
    19  // commit.
    20  type gitRepoSource struct {
    21  	repo   *git.Repository
    22  	commit *object.Commit
    23  	roots  map[string]string
    24  }
    25  
    26  // newGitRepoSource returns a new gitRepoSource for the given git repository
    27  // path and commit, which can be a branch, tag, commit hash or other "treeish".
    28  func newGitRepoSource(path string, treeish string) (*gitRepoSource, error) {
    29  	repo, err := git.PlainOpen(path)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  	commitHash, err := repo.ResolveRevision(plumbing.Revision(treeish))
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  	commit, err := repo.CommitObject(*commitHash)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	return &gitRepoSource{repo: repo, commit: commit, roots: map[string]string{}}, nil
    42  }
    43  
    44  // Name implements FileSource.
    45  func (g *gitRepoSource) Name() string {
    46  	return "commit " + g.commit.Hash.String()
    47  }
    48  
    49  // Match implements FileSource.
    50  func (g *gitRepoSource) Match(rcConfig *config.ResourceSet) ([]string, error) {
    51  	tree, err := g.repo.TreeObject(g.commit.TreeHash)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	var matches []string
    56  	matchPattern := rcConfig.Path + "/**/spec.yaml"
    57  	err = tree.Files().ForEach(func(f *object.File) error {
    58  		// Check if this file matches
    59  		if ok, err := doublestar.Match(matchPattern, f.Name); err != nil {
    60  			return err
    61  		} else if !ok {
    62  			return nil
    63  		}
    64  		// Check exclude patterns
    65  		for i := range rcConfig.Excludes {
    66  			if ok, err := doublestar.Match(rcConfig.Excludes[i], f.Name); err != nil {
    67  				return err
    68  			} else if ok {
    69  				return nil
    70  			}
    71  		}
    72  		matches = append(matches, f.Name)
    73  		return nil
    74  	})
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	return matches, nil
    79  }
    80  
    81  // Prefetch implements FileSource.
    82  func (g *gitRepoSource) Prefetch(root string) (string, error) {
    83  	tree, err := g.commit.Tree()
    84  	if err != nil {
    85  		return "", err
    86  	}
    87  	tree, err = tree.Tree(root)
    88  	if err != nil {
    89  		return "", err
    90  	}
    91  	tempDir, err := os.MkdirTemp("", "")
    92  	if err != nil {
    93  		return "", err
    94  	}
    95  	err = func() error {
    96  		// Wrap this in a closure to simplify walker cleanup
    97  		w := object.NewTreeWalker(tree, true, map[plumbing.Hash]bool{})
    98  		defer w.Close()
    99  		for {
   100  			ok, err := func() (bool, error) {
   101  				// Wrap this in a closure to release fds early & often
   102  				name, entry, err := w.Next()
   103  				if err == io.EOF {
   104  					return false, nil
   105  				} else if err != nil {
   106  					return false, err
   107  				}
   108  				if !entry.Mode.IsFile() {
   109  					return true, nil
   110  				}
   111  				blob, err := object.GetBlob(g.repo.Storer, entry.Hash)
   112  				if err != nil {
   113  					return false, err
   114  				}
   115  				err = os.MkdirAll(filepath.Join(tempDir, filepath.Dir(name)), 0777)
   116  				if err != nil {
   117  					return false, err
   118  				}
   119  				tempFile, err := os.Create(filepath.Join(tempDir, name))
   120  				if err != nil {
   121  					return false, err
   122  				}
   123  				defer tempFile.Close()
   124  				blobContents, err := blob.Reader()
   125  				if err != nil {
   126  					return false, err
   127  				}
   128  				_, err = io.Copy(tempFile, blobContents)
   129  				if err != nil {
   130  					return false, err
   131  				}
   132  				return true, nil
   133  			}()
   134  			if err != nil {
   135  				return err
   136  			}
   137  			if !ok {
   138  				return nil
   139  			}
   140  		}
   141  	}()
   142  	if err != nil {
   143  		// Clean up temp dir if we failed to populate it
   144  		errs := multierr.Append(nil, err)
   145  		err := os.RemoveAll(tempDir)
   146  		if err != nil {
   147  			errs = multierr.Append(errs, err)
   148  		}
   149  		return "", errs
   150  	}
   151  	g.roots[root] = tempDir
   152  	return tempDir, nil
   153  }
   154  
   155  // Fetch implements FileSource.
   156  func (g *gitRepoSource) Fetch(path string) (string, error) {
   157  	var matchRoot string
   158  	// Linear search for this is probably good enough. Could use a trie if it
   159  	// gets out of hand.
   160  	for root := range g.roots {
   161  		if strings.HasPrefix(path, root) {
   162  			matchRoot = root
   163  		}
   164  	}
   165  	if matchRoot == "" {
   166  		return "", nil
   167  	}
   168  	matchPath := strings.Replace(path, matchRoot, g.roots[matchRoot], 1)
   169  	if _, err := os.Stat(matchPath); os.IsNotExist(err) {
   170  		return "", nil
   171  	}
   172  	return matchPath, nil
   173  }
   174  
   175  // Close implements fileSource.
   176  func (g *gitRepoSource) Close() error {
   177  	var errs error
   178  	for _, tempDir := range g.roots {
   179  		errs = multierr.Append(errs, os.RemoveAll(tempDir))
   180  	}
   181  	return errs
   182  }