github.com/snyk/vervet/v3@v3.7.0/internal/linter/optic/git.go (about)

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