├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── main.go └── packages.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Steven Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARIES=$$(go list ./...) 2 | TESTABLE=$$(go list ./...) 3 | 4 | all : test build 5 | 6 | deps: 7 | @dep ensure && dep ensure -update 8 | .PHONY: deps 9 | 10 | build: 11 | @go install -v $(BINARIES) 12 | .PHONY: build 13 | 14 | test: 15 | @go test -v $(TESTABLE) 16 | .PHONY: test 17 | 18 | package: 19 | @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o linux.amd64.tainted -ldflags="-s -w" . && \ 20 | tar czf linux.amd64.tainted.tar.gz linux.amd64.tainted && \ 21 | rm linux.amd64.tainted 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tainted 2 | 3 | A tool to compare which go packages will need be to rebuilt as a result of changes between two git diffs. 4 | 5 | Ideally used as part of a CI/CD pipeline to see which servies should be rebuilt and redeployed 6 | 7 | N.B. Name inspired by terraforms taint terminology 8 | 9 | # Project status 10 | 11 | I do not have time to update or maintain this package but fortunatly Digital ocean have a much better and activly maintained tool here which does what atinted does and more, 12 | 13 | https://github.com/digitalocean/gta 14 | 15 | https://www.digitalocean.com/blog/gta-detecting-affected-dependent-go-packages/ 16 | 17 | # Requirments 18 | - git MUST be installed and be on the path where tainted is run 19 | 20 | # Install 21 | 22 | ### From Go 23 | go get -u github.com/kynrai/tainted 24 | 25 | ### From binaries 26 | see releases for latest binaries 27 | 28 | # Usage 29 | 30 | ### Basic usage 31 | From the go project repo e.g. `$GOPATH/src/github.com/user/repo/` 32 | 33 | go list ./... | tainted 34 | 35 | It using the standard go repo layout (recommended) 36 | 37 | go list ./cmd/... | tainted 38 | 39 | You can manually set the git commit ranges, by default the previous commit is checked with HEAD. i.e. `HEAD~1..HEAD` 40 | 41 | You can change any or all of the params 42 | 43 | go list ./... | tainted -from=HEAD~2 44 | 45 | go list ./... | tainted -from=HEAD~1 -to=HEAD~1 46 | 47 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "go/build" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "sort" 13 | "strings" 14 | ) 15 | 16 | func usage() { 17 | fmt.Println(`Usage: | tainted 18 | 19 | Example: 20 | go list ./... | tainted 21 | 22 | This program takes a list of packages from stdin and returns a list of packages 23 | which have beend tained and need to be rebuilt. A package is tained when one or 24 | more of its dependacies have been modified`) 25 | fmt.Println() 26 | flag.PrintDefaults() 27 | } 28 | 29 | var ( 30 | packages map[string]struct{} // the packages to check for taint 31 | changedDirs map[string]struct{} // the directories which contain modified files 32 | cache map[string]*build.Package // a map[>package name>] to skip repeat lookups 33 | gitDirPtr *string // the git directory to check for changes 34 | commitFromPtr *string // the earliest commit to diff 35 | commitToPtr *string // the latest commit to diff 36 | includeTestFiles *bool // this will include test files for evaluation 37 | ) 38 | 39 | func init() { 40 | cache = make(map[string]*build.Package) 41 | changedDirs = make(map[string]struct{}) 42 | packages = make(map[string]struct{}) 43 | } 44 | 45 | func main() { 46 | gitDirPtr = flag.String("dir", ".", "the git directory to check") 47 | commitFromPtr = flag.String("from", "HEAD~1", "commit to take changes from") 48 | commitToPtr = flag.String("to", "HEAD", "commit to take changes to") 49 | includeTestFiles = flag.Bool("test", false, "include test files") 50 | 51 | flag.Usage = usage 52 | flag.Parse() 53 | 54 | // check if we have anything coming from stdin 55 | stat, err := os.Stdin.Stat() 56 | if err != nil { 57 | fmt.Printf("failed to read from stdin: %s", err) 58 | os.Exit(1) 59 | } 60 | if (stat.Mode() & os.ModeNamedPipe) == 0 { 61 | flag.Usage() 62 | os.Exit(0) 63 | } 64 | 65 | // populate the changed directories slice 66 | modified() 67 | 68 | // read packages from stdin 69 | readPackages() 70 | 71 | // for each package we want to get its full deps tree and see if it 72 | // contains any elements from the changedDirs 73 | cwd, err := os.Getwd() 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | output := make(map[string]struct{}) 78 | for k := range packages { 79 | // get all the deps 80 | deps, err := findDeps(k, cwd) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | if hasChanges(deps) { 85 | output[k] = struct{}{} 86 | } 87 | } 88 | // finally to make it all pretty, sort it in a slice 89 | prettyOutput := make([]string, 0, len(output)) 90 | for k := range output { 91 | prettyOutput = append(prettyOutput, k) 92 | } 93 | if len(prettyOutput) == 0 { 94 | return 95 | } 96 | sort.Strings(prettyOutput) 97 | fmt.Println(strings.Join(prettyOutput, "\n")) 98 | } 99 | 100 | // checks to see if any of the deps have the same suffix as anything in the changedDirs 101 | func hasChanges(deps []string) bool { 102 | for _, v := range deps { 103 | for k := range changedDirs { 104 | if strings.HasSuffix(v, k) { 105 | return true 106 | } 107 | } 108 | } 109 | return false 110 | } 111 | 112 | // read all the packages from stdin into the global packages var 113 | func readPackages() { 114 | scanner := bufio.NewScanner(os.Stdin) 115 | for scanner.Scan() { 116 | packages[scanner.Text()] = struct{}{} 117 | } 118 | if err := scanner.Err(); err != nil { 119 | log.Fatal(err) 120 | } 121 | } 122 | 123 | // modified will use git to find out which folders have been changed 124 | func modified() { 125 | cmdArgs := []string{ 126 | "--no-pager", 127 | "-C", 128 | *gitDirPtr, 129 | "diff", 130 | "--name-only", 131 | *commitFromPtr, 132 | *commitToPtr, 133 | } 134 | 135 | cmd := exec.Command("git", cmdArgs...) 136 | cmdReader, err := cmd.StdoutPipe() 137 | if err != nil { 138 | log.Fatal(err) 139 | } 140 | 141 | scanner := bufio.NewScanner(cmdReader) 142 | go func() { 143 | for scanner.Scan() { 144 | scanned := scanner.Text() 145 | if !*includeTestFiles && strings.Contains(scanned, "_test.go") { 146 | continue 147 | } 148 | 149 | if dir := filepath.Dir(scanned); dir != "." { 150 | changedDirs[dir] = struct{}{} 151 | } 152 | } 153 | }() 154 | 155 | if err := cmd.Start(); err != nil { 156 | log.Fatal(err) 157 | } 158 | if err := cmd.Wait(); err != nil { 159 | log.Fatal(err) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /packages.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/build" 5 | "sort" 6 | ) 7 | 8 | type context struct { 9 | soFar map[string]struct{} 10 | ctx build.Context 11 | } 12 | 13 | func (c *context) find(name, dir string) (err error) { 14 | if name == "C" { 15 | return nil 16 | } 17 | var pkg *build.Package 18 | pkg, ok := cache[name] 19 | if !ok { 20 | pkg, err = c.ctx.Import(name, dir, 0) 21 | if err != nil { 22 | return err 23 | } 24 | } 25 | cache[name] = pkg 26 | if pkg.Goroot { 27 | return nil 28 | } 29 | 30 | if name != "." { 31 | c.soFar[pkg.ImportPath] = struct{}{} 32 | } 33 | imports := pkg.Imports 34 | for _, imp := range imports { 35 | if _, ok := c.soFar[imp]; !ok { 36 | if err := c.find(imp, pkg.Dir); err != nil { 37 | return err 38 | } 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | func findDeps(name, dir string) ([]string, error) { 45 | ctx := build.Default 46 | 47 | c := &context{ 48 | soFar: make(map[string]struct{}), 49 | ctx: ctx, 50 | } 51 | if err := c.find(name, dir); err != nil { 52 | return nil, err 53 | } 54 | deps := make([]string, 0, len(c.soFar)) 55 | for p := range c.soFar { 56 | deps = append(deps, p) 57 | } 58 | sort.Strings(deps) 59 | return deps, nil 60 | } 61 | --------------------------------------------------------------------------------