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 }