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 }