├── .gitignore ├── .travis.yml ├── Bunchfile ├── LICENSE ├── Makefile ├── README.md ├── bunch.go ├── bunchfile.go ├── bunchfile_test.go ├── commands.go ├── env.go ├── packages.go ├── packages_test.go └── versions.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .vendor/ 27 | bunch 28 | build/ 29 | notes/ 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | before_install: 5 | - go get github.com/dkulchenko/bunch 6 | script: 7 | - $HOME/gopath/bin/bunch install 8 | - $HOME/gopath/bin/bunch go test 9 | -------------------------------------------------------------------------------- /Bunchfile: -------------------------------------------------------------------------------- 1 | github.com/dkulchenko/bunch !self 2 | 3 | github.com/codegangsta/cli de28b84 4 | github.com/fatih/color 533cd7f 5 | github.com/briandowns/spinner eb21a4a5 6 | github.com/hashicorp/go-version bb92dddf 7 | github.com/juju/errors 4567a5e6 8 | github.com/kardianos/osext 9 | 10 | github.com/stretchr/testify 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniil Kulchenko 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 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | all: build 4 | 5 | test: 6 | @bunch go test -v 7 | 8 | bootstrap: 9 | @gox -build-toolchain 10 | 11 | setup: 12 | @mkdir build bin || true 13 | go get -u github.com/mitchellh/gox 14 | 15 | self-build: 16 | @bunch go build -o bin/bunch . 17 | 18 | build: 19 | @go build -o bin/bunch . 20 | 21 | run: 22 | @bin/bunch 23 | 24 | clean: 25 | @rm bin/bunch >/dev/null 2>&1 || true 26 | 27 | fullclean: clean 28 | @rm -fr build/* 29 | 30 | create-zip: 31 | @mkdir -p build/bunch 32 | @mv bunch_$(build_os)$(dest_ext) build/bunch/bunch$(dest_ext) 33 | @cp README.md build/bunch/README 34 | @cd build && zip -r bunch_$(build_os).zip bunch 35 | @rm -r build/bunch 36 | 37 | build-linux: clean 38 | @gox -osarch="linux/386" 39 | @gox -osarch="linux/amd64" 40 | @$(MAKE) create-zip build_os=linux_386 41 | @$(MAKE) create-zip build_os=linux_amd64 42 | 43 | build-osx: clean 44 | @gox -os="darwin" 45 | @$(MAKE) create-zip build_os=darwin_386 46 | @$(MAKE) create-zip build_os=darwin_amd64 47 | 48 | build-windows: clean 49 | @gox -os="windows" 50 | @$(MAKE) create-zip build_os=windows_386 dest_ext=.exe 51 | @$(MAKE) create-zip build_os=windows_amd64 dest_ext=.exe 52 | 53 | build-all: fullclean build-linux build-windows build-osx clean 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bunch 2 | 3 | An npm-like tool for easily managing Go (golang) dependencies. 4 | 5 | [![Release](https://img.shields.io/github/release/dkulchenko/bunch.svg)](https://github.com/dkulchenko/bunch/releases) 6 | [![Build Status](https://img.shields.io/travis/dkulchenko/bunch.svg)](https://travis-ci.org/dkulchenko/bunch) 7 | 8 | ## Overview 9 | 10 | `go get` is very good. But managing multiple packages, locking versions down for reproducible builds, and 11 | changing GOPATH or rewriting package imports is an exercise left to the developer. bunch tackles this 12 | issue by providing a comprehensive tool for managing Go dependencies similar to npm for Node, supporting 13 | installing, uninstalling, pruning, listing outdated packages, and much more. 14 | 15 | ## Installation 16 | 17 | ``` 18 | go get github.com/dkulchenko/bunch 19 | ``` 20 | 21 | Alternatively, [precompiled binaries](https://github.com/dkulchenko/bunch/releases) for 22 | supported operating systems are available. 23 | 24 | You can pin bunch itself, if you'd like to lock the version of bunch down. If bunch finds a version 25 | of itself in the vendored environment (that is, .vendor/bin/bunch exists), it will use that version instead. 26 | 27 | ## Bunchfile 28 | 29 | See this [repo's Bunchfile](https://github.com/dkulchenko/bunch/blob/master/Bunchfile) as an example. 30 | 31 | ``` 32 | github.com/this/repo !self 33 | 34 | # comments are fun! 35 | github.com/another/package 36 | github.com/another/package2 v2 # can be a branch, tag, or commit 37 | github.com/another/package3 a2b5va78d 38 | 39 | github.com/another/package4 >= 1.0 40 | github.com/another/package5 ~> 1.4.x 41 | github.com/another/package6 > 1.0, < 1.4 42 | ``` 43 | 44 | ## Usage 45 | 46 | ### Managing packages 47 | 48 | (optional) Generate a Bunchfile based on your existing imports to get you started: 49 | 50 | ``` 51 | bunch generate 52 | ``` 53 | 54 | Install all packages listed in Bunchfile to .vendor directory: 55 | 56 | ``` 57 | bunch install 58 | ``` 59 | 60 | Install a specific package and save it to the Bunchfile: 61 | 62 | ``` 63 | bunch install github.com/abc/xyz 64 | bunch install github.com/abc/xyz --save 65 | bunch install github.com/abc/xyz@v2 --save 66 | bunch install github.com/abc/xyz github.com/abc/xyz2 67 | ``` 68 | 69 | Update all packages to latest versions matching Bunchfile: 70 | 71 | ``` 72 | bunch update 73 | ``` 74 | 75 | Remove a package and save the change to the Bunchfile: 76 | 77 | ``` 78 | bunch uninstall github.com/abc/xyz 79 | bunch uninstall github.com/abc/xyz --save 80 | ``` 81 | 82 | Prune unused packages from vendor directory (similar to npm prune): 83 | 84 | ``` 85 | bunch prune 86 | ``` 87 | 88 | List outdated packages: 89 | 90 | ``` 91 | bunch outdated 92 | ``` 93 | 94 | Lock down the commits currently in use (creates Bunchfile.lock; similar to npm shrinkwrap): 95 | 96 | ``` 97 | bunch lock 98 | ``` 99 | 100 | Rebuild (recompile) all packages: 101 | 102 | ``` 103 | bunch rebuild 104 | ``` 105 | 106 | ### Using the vendored environment 107 | 108 | Run commands/builds within the vendored environment (sets $GOPATH and $PATH): 109 | 110 | ``` 111 | bunch go build . 112 | bunch go fmt 113 | bunch exec make 114 | bunch shell 115 | ``` 116 | 117 | You can also add this to your .bash_profile to make the `go` command automagically be bunch-aware (e.g. go build will automatically have the correct $GOPATH set if a Bunchfile is present): 118 | 119 | ``` 120 | if which bunch > /dev/null; then eval "$(bunch shim -)"; fi 121 | ``` 122 | 123 | ## Limitations 124 | 125 | For basic operations like installing/uninstalling/updating/pruning packages, all VCSes supported by `go get` are supported by bunch (git, hg, svn, and bzr). 126 | 127 | For more advanced operations, packages using Git are fully supported. Mercurial has mostly full support (does not support version ranges in the Bunchfile). Bazaar has some support (does not support version ranges, "bunch outdated", or "bunch install" caching up-to-date packages). Subversion has rudimentary support (only install/update/uninstall/prune). 128 | 129 | ## Contribute 130 | 131 | Patches welcome :) 132 | 133 | - Fork repository 134 | - Create a feature or bugfix branch 135 | - Open a new pull request 136 | 137 | ## License 138 | 139 | The MIT License (MIT) 140 | 141 | Copyright (c) 2015 Daniil Kulchenko 142 | -------------------------------------------------------------------------------- /bunch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path" 8 | "time" 9 | 10 | "github.com/codegangsta/cli" 11 | "github.com/kardianos/osext" 12 | ) 13 | 14 | var InitialPath string 15 | var InitialGoPath string 16 | 17 | var Verbose bool 18 | 19 | var SpinnerCharSet = 14 20 | var SpinnerInterval = 50 * time.Millisecond 21 | 22 | func main() { 23 | currentExecutable, _ := osext.Executable() 24 | vendoredBunchPath := path.Join(".vendor", "bin", "bunch") 25 | 26 | fi1, errStat1 := os.Stat(currentExecutable) 27 | fi2, errStat2 := os.Stat(vendoredBunchPath) 28 | 29 | if exists, _ := pathExists(vendoredBunchPath); errStat1 == nil && errStat2 == nil && exists && !os.SameFile(fi1, fi2) { 30 | cmd := exec.Command(vendoredBunchPath, os.Args[1:]...) 31 | cmd.Stdin = os.Stdin 32 | cmd.Stdout = os.Stdout 33 | cmd.Stderr = os.Stderr 34 | 35 | err := cmd.Run() 36 | if err == nil { 37 | if cmd.ProcessState.Success() { 38 | os.Exit(0) 39 | } 40 | } 41 | 42 | // if "subbunch" succeeded, exit, otherwise, continue with regular bunch business 43 | fmt.Println("vendored bunch exited with a non-zero exit status, trying again with global bunch") 44 | } 45 | 46 | InitialPath = os.Getenv("PATH") 47 | InitialGoPath = os.Getenv("GOPATH") 48 | 49 | app := cli.NewApp() 50 | app.Name = "bunch" 51 | app.Usage = "npm-like tool for managing Go dependencies" 52 | app.Version = "0.6" 53 | app.Authors = []cli.Author{cli.Author{Name: "Daniil Kulchenko", Email: "daniil@kulchenko.com"}} 54 | 55 | app.Flags = []cli.Flag{ 56 | cli.BoolFlag{ 57 | Name: "verbose", 58 | Usage: "output more information", 59 | }, 60 | } 61 | 62 | app.Before = func(context *cli.Context) error { 63 | Verbose = context.GlobalBool("verbose") 64 | 65 | return nil 66 | } 67 | 68 | app.Commands = []cli.Command{ 69 | { 70 | Name: "install", 71 | Aliases: []string{"i"}, 72 | Usage: "install package(s)", 73 | Flags: []cli.Flag{ 74 | cli.BoolFlag{ 75 | Name: "save", 76 | Usage: "save installed package to Bunchfile", 77 | }, 78 | cli.BoolFlag{ 79 | Name: "g", 80 | Usage: "install package to global $GOPATH instead of vendored directory", 81 | }, 82 | }, 83 | Action: func(c *cli.Context) error { 84 | installCommand(c, false, true, true) 85 | return nil 86 | }, 87 | }, 88 | { 89 | Name: "update", 90 | Aliases: []string{"u"}, 91 | Usage: "update package(s)", 92 | Action: func(c *cli.Context) error { 93 | installCommand(c, true, true, false) 94 | return nil 95 | }, 96 | }, 97 | { 98 | Name: "uninstall", 99 | Aliases: []string{"r"}, 100 | Usage: "uninstall package(s)", 101 | Flags: []cli.Flag{ 102 | cli.BoolFlag{ 103 | Name: "save", 104 | Usage: "save uninstalled package to Bunchfile", 105 | }, 106 | cli.BoolFlag{ 107 | Name: "g", 108 | Usage: "uninstall package from global $GOPATH instead of vendored directory", 109 | }, 110 | }, 111 | Action: func(c *cli.Context) error { 112 | uninstallCommand(c) 113 | return nil 114 | }, 115 | }, 116 | { 117 | Name: "prune", 118 | Usage: "remove packages not referenced in Bunchfile", 119 | Action: func(c *cli.Context) error { 120 | pruneCommand(c) 121 | return nil 122 | }, 123 | }, 124 | { 125 | Name: "outdated", 126 | Usage: "list outdated packages", 127 | Action: func(c *cli.Context) error { 128 | outdatedCommand(c) 129 | return nil 130 | }, 131 | }, 132 | { 133 | Name: "lock", 134 | Usage: "generate a file locking down current versions of dependencies", 135 | Action: func(c *cli.Context) error { 136 | lockCommand(c) 137 | return nil 138 | }, 139 | }, 140 | { 141 | Name: "rebuild", 142 | Usage: "rebuild all dependencies", 143 | Action: func(c *cli.Context) error { 144 | installCommand(c, true, false, true) 145 | return nil 146 | }, 147 | }, 148 | { 149 | Name: "generate", 150 | Usage: "generate a Bunchfile based on package imports in current directory", 151 | Action: func(c *cli.Context) error { 152 | generateCommand(c) 153 | return nil 154 | }, 155 | }, 156 | { 157 | Name: "go", 158 | Usage: "run a Go command within the vendor environment (e.g. bunch go fmt)", 159 | SkipFlagParsing: true, 160 | Action: func(c *cli.Context) error { 161 | goCommand(c) 162 | return nil 163 | }, 164 | }, 165 | { 166 | Name: "exec", 167 | Usage: "run any command within the vendor environment (e.g. bunch exec make)", 168 | SkipFlagParsing: true, 169 | Action: func(c *cli.Context) error { 170 | execCommand(c) 171 | return nil 172 | }, 173 | }, 174 | { 175 | Name: "shell", 176 | Usage: "start a shell within the vendor environment", 177 | SkipFlagParsing: true, 178 | Action: func(c *cli.Context) error { 179 | shellCommand(c) 180 | return nil 181 | }, 182 | }, 183 | { 184 | Name: "shim", 185 | Usage: "sourced in .bash_profile to alias the 'go' tool", 186 | Action: func(c *cli.Context) error { 187 | shimCommand(c) 188 | return nil 189 | }, 190 | }, 191 | } 192 | 193 | app.Run(os.Args) 194 | } 195 | -------------------------------------------------------------------------------- /bunchfile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "go/build" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/fatih/color" 15 | "github.com/juju/errors" 16 | ) 17 | 18 | type Package struct { 19 | Repo string 20 | Version string 21 | LockedVersion string 22 | 23 | IsSelf bool 24 | IsLink bool 25 | LinkTarget string 26 | } 27 | 28 | type BunchFile struct { 29 | Packages []Package 30 | Raw []string 31 | } 32 | 33 | var commentStripRegexp = regexp.MustCompile(`#.*`) 34 | var versionSwapRegexp = regexp.MustCompile(`^(\S+)\s*(.*)`) 35 | 36 | func (b *BunchFile) RawIndex(repo string) (int, bool) { 37 | for i, packString := range b.Raw { 38 | parts := strings.Fields(packString) 39 | 40 | if len(parts) < 1 { 41 | continue 42 | } 43 | 44 | if parts[0] == repo { 45 | return i, true 46 | } 47 | } 48 | 49 | return 0, false 50 | } 51 | 52 | func (b *BunchFile) PackageIndex(repo string) (int, bool) { 53 | for i, pack := range b.Packages { 54 | if pack.Repo == repo { 55 | return i, true 56 | } 57 | } 58 | 59 | return 0, false 60 | } 61 | 62 | func (b *BunchFile) AddPackage(packString string) error { 63 | pack := parsePackage(packString) 64 | 65 | index, present := b.RawIndex(pack.Repo) 66 | 67 | if present { 68 | packIndex, _ := b.PackageIndex(pack.Repo) 69 | b.Packages[packIndex] = pack 70 | 71 | initialLine := b.Raw[index] 72 | 73 | replacementString := fmt.Sprintf("$1 %s", pack.Version) 74 | newLine := versionSwapRegexp.ReplaceAllString(initialLine, replacementString) 75 | 76 | b.Raw[index] = newLine 77 | } else { 78 | b.Packages = append(b.Packages, pack) 79 | raw := []string{pack.Repo} 80 | if pack.Version != "" { 81 | raw = append(raw, pack.Version) 82 | } 83 | 84 | b.Raw = append(b.Raw, strings.Join(raw, " ")) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (b *BunchFile) RemovePackage(packString string) error { 91 | pack := parsePackage(packString) 92 | 93 | index, present := b.RawIndex(pack.Repo) 94 | 95 | if present { 96 | packIndex, packPresent := b.PackageIndex(pack.Repo) 97 | 98 | if packPresent { 99 | if packIndex < len(b.Packages)-1 { 100 | b.Packages = append(b.Packages[:packIndex], b.Packages[packIndex+1:]...) 101 | } else { 102 | b.Packages = b.Packages[:packIndex] 103 | } 104 | } 105 | 106 | if index < len(b.Raw)-1 { 107 | b.Raw = append(b.Raw[:index], b.Raw[index+1:]...) 108 | } else { 109 | b.Raw = b.Raw[:index] 110 | } 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func (b *BunchFile) Save() error { 117 | err := ioutil.WriteFile("Bunchfile", []byte(strings.Join(append(b.Raw, ""), "\n")), 0644) 118 | 119 | if err != nil { 120 | return errors.Trace(err) 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func createBunchfile() *BunchFile { 127 | return &BunchFile{} 128 | } 129 | 130 | func readBunchfile() (*BunchFile, error) { 131 | bunchbytes, err := ioutil.ReadFile("Bunchfile") 132 | 133 | if err != nil { 134 | return &BunchFile{}, errors.Trace(err) 135 | } 136 | 137 | bunch := BunchFile{ 138 | Raw: strings.Split(strings.TrimSpace(string(bunchbytes)), "\n"), 139 | } 140 | 141 | lockedCommits := make(map[string]string) 142 | 143 | if exists, _ := pathExists("Bunchfile.lock"); exists { 144 | lockBytes, err := ioutil.ReadFile("Bunchfile.lock") 145 | if err != nil { 146 | return &BunchFile{}, errors.Trace(err) 147 | } 148 | 149 | err = json.Unmarshal(lockBytes, &lockedCommits) 150 | if err != nil { 151 | return &BunchFile{}, errors.Trace(err) 152 | } 153 | } 154 | 155 | for _, line := range bunch.Raw { 156 | line = commentStripRegexp.ReplaceAllLiteralString(line, "") 157 | line = strings.TrimSpace(line) 158 | 159 | if line == "" { 160 | continue 161 | } 162 | 163 | pack := Package{} 164 | 165 | packageInfo := strings.SplitN(line, " ", 2) 166 | 167 | if len(packageInfo) < 1 { 168 | continue 169 | } else { 170 | pack.Repo = packageInfo[0] 171 | } 172 | 173 | if len(packageInfo) >= 2 { 174 | pack.Version = packageInfo[1] 175 | } 176 | 177 | if strings.HasPrefix(pack.Version, "!link") || strings.HasPrefix(pack.Version, "!self") { 178 | if strings.HasPrefix(pack.Version, "!self") { 179 | pack.IsSelf = true 180 | } 181 | 182 | pack.IsLink = true 183 | 184 | linkList := strings.Split(pack.Version, ":") 185 | 186 | if len(linkList) == 2 { 187 | pack.LinkTarget = linkList[1] 188 | } else { 189 | wd, err := os.Getwd() 190 | if err != nil { 191 | return &BunchFile{}, errors.Trace(err) 192 | } 193 | 194 | pack.LinkTarget = wd 195 | } 196 | } 197 | 198 | if lockedCommits[pack.Repo] != "" { 199 | pack.LockedVersion = lockedCommits[pack.Repo] 200 | } 201 | 202 | bunch.Packages = append(bunch.Packages, pack) 203 | } 204 | 205 | return &bunch, nil 206 | } 207 | 208 | func filterCommonBasePackages(depList []string, selfBase string) []string { 209 | basePackages := []string{} 210 | 211 | for i, dep1 := range depList { 212 | foundPrefix := false 213 | 214 | if strings.HasPrefix(dep1, selfBase) { 215 | continue 216 | } 217 | 218 | for j, dep2 := range depList { 219 | if i == j { 220 | continue 221 | } 222 | 223 | if strings.HasPrefix(dep1, dep2) { 224 | foundPrefix = true 225 | break 226 | } 227 | } 228 | 229 | if !foundPrefix { 230 | basePackages = append(basePackages, dep1) 231 | } 232 | } 233 | 234 | return basePackages 235 | } 236 | 237 | func generateBunchfile() error { 238 | bunch := BunchFile{} 239 | 240 | goListCommand := []string{"go", "list", "--json", "."} 241 | output, err := exec.Command(goListCommand[0], goListCommand[1:]...).Output() 242 | if err != nil { 243 | return errors.Trace(err) 244 | } 245 | 246 | packageInfo := GoList{} 247 | err = json.Unmarshal(output, &packageInfo) 248 | 249 | if err != nil { 250 | return errors.Trace(err) 251 | } 252 | 253 | err = bunch.AddPackage(fmt.Sprintf("%s@!self", packageInfo.ImportPath)) 254 | if err != nil { 255 | return errors.Trace(err) 256 | } 257 | 258 | for _, dep := range filterCommonBasePackages(append(packageInfo.Deps, packageInfo.TestImports...), packageInfo.ImportPath) { 259 | // check that the package is not part of the standard library 260 | if exists, _ := pathExists(path.Join(build.Default.GOROOT, "src", dep)); !exists { 261 | err = bunch.AddPackage(dep) 262 | if err != nil { 263 | return errors.Trace(err) 264 | } 265 | } 266 | } 267 | 268 | err = bunch.Save() 269 | if err != nil { 270 | return errors.Trace(err) 271 | } 272 | 273 | color.Green("Bunchfile generated successfully") 274 | 275 | return nil 276 | } 277 | -------------------------------------------------------------------------------- /bunchfile_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFilterCommonBasePackages(t *testing.T) { 10 | depList := []string{"github.com/a/b/c", "github.com/d/e/f", "github.com/a/b"} 11 | result := filterCommonBasePackages(depList, "github.com/my/package") 12 | 13 | expectedResult := []string{"github.com/d/e/f", "github.com/a/b"} 14 | 15 | assert.Equal(t, result, expectedResult, "packages with a common base should be filtered out") 16 | } 17 | 18 | func TestCreateBunchfile(t *testing.T) { 19 | assert.Equal(t, &BunchFile{}, createBunchfile(), "create bunchfile returns a bunchfile object") 20 | } 21 | 22 | func TestRawIndex(t *testing.T) { 23 | bunch := createBunchfile() 24 | 25 | bunch.Raw = []string{"github.com/a/b", "github.com/a/b/c", "github.com/d 123456"} 26 | 27 | rawIndex, present := bunch.RawIndex("github.com/a/b") 28 | assert.Equal(t, rawIndex, 0, "found github.com/a/b at first position in bunchfile") 29 | assert.Equal(t, present, true, "found github.com/a/b at first position in bunchfile") 30 | 31 | rawIndex, present = bunch.RawIndex("github.com/d") 32 | assert.Equal(t, rawIndex, 2, "found github.com/d at third position in bunchfile") 33 | assert.Equal(t, present, true, "found github.com/a/b at first position in bunchfile") 34 | 35 | rawIndex, present = bunch.RawIndex("github.com/a/b/c") 36 | assert.Equal(t, rawIndex, 1, "found github.com/a/b/c at second position in bunchfile") 37 | assert.Equal(t, present, true, "found github.com/a/b/c in bunchfile") 38 | 39 | _, present = bunch.RawIndex("github.com/unknown") 40 | assert.Equal(t, present, false, "did not find github.com/unknown in bunchfile") 41 | } 42 | 43 | func TestPackageIndex(t *testing.T) { 44 | bunch := createBunchfile() 45 | 46 | bunch.Packages = []Package{ 47 | Package{ 48 | Repo: "github.com/a/b", 49 | Version: "123", 50 | }, 51 | Package{ 52 | Repo: "github.com/def", 53 | }, 54 | Package{ 55 | Repo: "github.com/a/b/c", 56 | Version: "xyz", 57 | }, 58 | } 59 | 60 | packageIndex, present := bunch.PackageIndex("github.com/a/b") 61 | assert.Equal(t, packageIndex, 0, "found github.com/a/b in first position in bunchfile package list") 62 | assert.Equal(t, present, true, "found github.com/a/b in bunchfile package list") 63 | 64 | packageIndex, present = bunch.PackageIndex("github.com/def") 65 | assert.Equal(t, packageIndex, 1, "found github.com/def at second position in bunchfile package list") 66 | assert.Equal(t, present, true, "found github.com/def in bunchfile package list") 67 | 68 | packageIndex, present = bunch.PackageIndex("github.com/a/b/c") 69 | assert.Equal(t, packageIndex, 2, "found github.com/a/b/c at third position in bunchfile package list") 70 | assert.Equal(t, present, true, "found github.com/a/b/c in bunchfile package list") 71 | 72 | _, present = bunch.PackageIndex("github.com/unknown") 73 | assert.Equal(t, present, false, "did not find github.com/unknown in bunchfile package list") 74 | } 75 | 76 | func TestAddPackageNew(t *testing.T) { 77 | bunch := createBunchfile() 78 | 79 | err := bunch.AddPackage("github.com/a/b/c") 80 | assert.Nil(t, err, "did not error on adding package") 81 | 82 | packageIndex, present := bunch.PackageIndex("github.com/a/b/c") 83 | assert.Equal(t, packageIndex, 0, "found github.com/a/b/c in first position in bunchfile package list") 84 | assert.Equal(t, present, true, "found github.com/a/b/c in bunchfile package list") 85 | 86 | err = bunch.AddPackage("github.com/a/b/d@v1.2.0") 87 | assert.Nil(t, err, "did not error on adding package") 88 | 89 | packageIndex, present = bunch.PackageIndex("github.com/a/b/d") 90 | assert.Equal(t, packageIndex, 1, "found github.com/a/b/d in second position in bunchfile package list") 91 | assert.Equal(t, present, true, "found github.com/a/b/d in bunchfile package list") 92 | } 93 | 94 | func TestAddPackageUpdate(t *testing.T) { 95 | bunch := createBunchfile() 96 | 97 | err := bunch.AddPackage("github.com/a/b/c") 98 | assert.Nil(t, err, "did not error on adding package") 99 | 100 | pack := bunch.Packages[0] 101 | assert.Equal(t, len(bunch.Packages), 1, "should only be one package in bunchfile") 102 | assert.Equal(t, "", pack.Version, "version should not be set yet") 103 | 104 | err = bunch.AddPackage("github.com/a/b/c@v1.2.1") 105 | assert.Nil(t, err, "did not error on adding package") 106 | 107 | pack = bunch.Packages[0] 108 | assert.Equal(t, len(bunch.Packages), 1, "should still be only one package in bunchfile") 109 | assert.Equal(t, pack.Version, "v1.2.1", "version should now be set") 110 | } 111 | 112 | func TestRemovePackage(t *testing.T) { 113 | bunch := createBunchfile() 114 | 115 | err := bunch.AddPackage("github.com/a/b/c") 116 | assert.Nil(t, err, "did not error on adding package") 117 | 118 | assert.Equal(t, len(bunch.Raw), 1, "should be one packages in Bunchfile") 119 | assert.Equal(t, len(bunch.Packages), 1, "should be one package in bunchfile package list") 120 | 121 | err = bunch.RemovePackage("github.com/a/b/c") 122 | assert.Nil(t, err, "did not error on removing package") 123 | 124 | assert.Equal(t, len(bunch.Raw), 0, "should be no packages in Bunchfile") 125 | assert.Equal(t, len(bunch.Packages), 0, "should be no packages in Bunchfile package list") 126 | } 127 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "strings" 11 | 12 | "github.com/codegangsta/cli" 13 | "github.com/juju/errors" 14 | ) 15 | 16 | func setupVendoring() error { 17 | vendorDirs := []string{".vendor/bin", ".vendor/pkg", ".vendor/src"} 18 | 19 | for _, vendorDir := range vendorDirs { 20 | err := os.MkdirAll(vendorDir, 0755) 21 | 22 | if err != nil { 23 | return errors.Trace(err) 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func installCommand(c *cli.Context, forceUpdate bool, checkUpstream bool, respectLocked bool) { 31 | // bunch install 32 | // bunch install github.com/abc/xyz 33 | // bunch install github.com/abc/xyz github.com/abc/def 34 | // bunch install github.com/abc/xyz --save 35 | // bunch install github.com/abc/xyz -g 36 | // bunch install abc/xyz # github shorthand 37 | 38 | // bunch update 39 | // bunch update github.com/abc/xyz 40 | // bunch update github.com/abc/xyz github.com/abc/def 41 | // bunch update github.com/abc/xyz --save 42 | // bunch update github.com/abc/xyz -g 43 | 44 | packages := c.Args() 45 | 46 | err := setupVendoring() 47 | if err != nil { 48 | log.Fatalf("unable to set up vendor dirs: %s", err) 49 | } 50 | 51 | if len(packages) == 0 { 52 | bunch, err := readBunchfile() 53 | if err != nil { 54 | log.Fatalf("unable to read Bunchfile: %s", err) 55 | } 56 | 57 | err = installPackagesFromBunchfile(bunch, forceUpdate, checkUpstream, respectLocked) 58 | 59 | if err != nil { 60 | log.Fatalf("failed installing packages: %s %s", err, err.(*errors.Err).StackTrace()) 61 | } 62 | } else { 63 | global := c.Bool("g") 64 | save := c.Bool("save") 65 | 66 | if global && os.Getenv("GOPATH") == "" { 67 | log.Fatalf("GOPATH must be set when -g used") 68 | } 69 | 70 | var bunch *BunchFile 71 | if exists, _ := pathExists("Bunchfile"); exists { 72 | bunch, err = readBunchfile() 73 | if err != nil { 74 | log.Fatalf("unable to read Bunchfile: %s", err) 75 | } 76 | } else { 77 | bunch = createBunchfile() 78 | } 79 | 80 | err := installPackagesFromRepoStrings(packages, global, forceUpdate, checkUpstream, respectLocked) 81 | if err != nil { 82 | log.Fatalf("failed installing packages: %s", err) 83 | } 84 | 85 | if save { 86 | for _, pack := range packages { 87 | err := bunch.AddPackage(pack) 88 | 89 | if err != nil { 90 | log.Fatalf("failed adding package %s to save list: %s", pack, err) 91 | } 92 | } 93 | 94 | err = bunch.Save() 95 | if err != nil { 96 | log.Fatalf("failed saving Bunchfile: %s", err) 97 | } 98 | } 99 | } 100 | } 101 | 102 | func uninstallCommand(c *cli.Context) { 103 | // bunch uninstall github.com/abc/xyz 104 | // bunch uninstall github.com/abc/xyz --save 105 | // bunch uninstall github.com/abc/xyz -g 106 | 107 | packages := c.Args() 108 | 109 | err := setupVendoring() 110 | if err != nil { 111 | log.Fatalf("unable to set up vendor dirs: %s", err) 112 | } 113 | 114 | if len(packages) == 0 { 115 | log.Fatalf("uninstall requires an argument") 116 | } else { 117 | global := c.Bool("g") 118 | save := c.Bool("save") 119 | 120 | if global && os.Getenv("GOPATH") == "" { 121 | log.Fatalf("GOPATH must be set when -g used") 122 | } 123 | 124 | var bunch *BunchFile 125 | if exists, _ := pathExists("Bunchfile"); exists { 126 | bunch, err = readBunchfile() 127 | if err != nil { 128 | log.Fatalf("unable to read Bunchfile: %s", err) 129 | } 130 | } else { 131 | bunch = createBunchfile() 132 | } 133 | 134 | err := removePackages(packages, bunch, global) 135 | if err != nil { 136 | log.Fatalf("failed removing packages: %s", err) 137 | } 138 | 139 | if save { 140 | for _, pack := range packages { 141 | err := bunch.RemovePackage(pack) 142 | 143 | if err != nil { 144 | log.Fatalf("failed removing package %s from save list: %s", pack, err) 145 | } 146 | } 147 | 148 | err = bunch.Save() 149 | if err != nil { 150 | log.Fatalf("failed saving Bunchfile: %s", err) 151 | } 152 | } 153 | } 154 | } 155 | 156 | func pruneCommand(c *cli.Context) { 157 | // bunch prune 158 | 159 | err := setupVendoring() 160 | if err != nil { 161 | log.Fatalf("unable to set up vendor dirs: %s", err) 162 | } 163 | 164 | var bunch *BunchFile 165 | if exists, _ := pathExists("Bunchfile"); exists { 166 | bunch, err = readBunchfile() 167 | if err != nil { 168 | log.Fatalf("unable to read Bunchfile: %s", err) 169 | } 170 | } else { 171 | log.Fatalf("can't prune without Bunchfile") 172 | } 173 | 174 | err = prunePackages(bunch) 175 | if err != nil { 176 | log.Fatalf("failed pruning packages: %s", err) 177 | } 178 | } 179 | 180 | func outdatedCommand(c *cli.Context) { 181 | // bunch outdated 182 | 183 | err := setupVendoring() 184 | if err != nil { 185 | log.Fatalf("unable to set up vendor dirs: %s", err) 186 | } 187 | 188 | var bunch *BunchFile 189 | if exists, _ := pathExists("Bunchfile"); exists { 190 | bunch, err = readBunchfile() 191 | if err != nil { 192 | log.Fatalf("unable to read Bunchfile: %s", err) 193 | } 194 | } else { 195 | log.Fatalf("can't check for outdated packages without Bunchfile") 196 | } 197 | 198 | err = checkOutdatedPackages(bunch) 199 | if err != nil { 200 | log.Fatalf("failed checking for outdated packages: %s %s", err, err.(*errors.Err).StackTrace()) 201 | } 202 | } 203 | 204 | func lockCommand(c *cli.Context) { 205 | // bunch lock 206 | 207 | err := setupVendoring() 208 | if err != nil { 209 | log.Fatalf("unable to set up vendor dirs: %s", err) 210 | } 211 | 212 | var bunch *BunchFile 213 | if exists, _ := pathExists("Bunchfile"); exists { 214 | bunch, err = readBunchfile() 215 | if err != nil { 216 | log.Fatalf("unable to read Bunchfile: %s", err) 217 | } 218 | } else { 219 | log.Fatalf("can't lock packages without Bunchfile") 220 | } 221 | 222 | err = lockPackages(bunch) 223 | if err != nil { 224 | log.Fatalf("failed locking packages: %s", err) 225 | } 226 | 227 | } 228 | 229 | func generateCommand(c *cli.Context) { 230 | // bunch generate 231 | 232 | err := setupVendoring() 233 | if err != nil { 234 | log.Fatalf("unable to set up vendor dirs: %s", err) 235 | } 236 | 237 | err = generateBunchfile() 238 | if err != nil { 239 | log.Fatalf("failed checking for outdated packages: %s", err) 240 | } 241 | } 242 | 243 | func goCommand(c *cli.Context) { 244 | // bunch go test 245 | // bunch go fmt 246 | // bunch go ... 247 | 248 | err := setVendorEnv() 249 | if err != nil { 250 | log.Fatalf("unable to set vendor env: %s", err) 251 | } 252 | 253 | cmd := exec.Command("go", c.Args()...) 254 | cmd.Stdin = os.Stdin 255 | cmd.Stdout = os.Stdout 256 | cmd.Stderr = os.Stderr 257 | 258 | err = cmd.Run() 259 | if err != nil { 260 | log.Fatalf("running 'go %s' failed: %s", strings.Join(c.Args(), " "), err) 261 | } 262 | } 263 | 264 | func execCommand(c *cli.Context) { 265 | // bunch exec make 266 | 267 | err := setVendorEnv() 268 | if err != nil { 269 | log.Fatalf("unable to set vendor env: %s", err) 270 | } 271 | 272 | cmd := exec.Command(c.Args()[0], c.Args()[1:]...) 273 | cmd.Stdin = os.Stdin 274 | cmd.Stdout = os.Stdout 275 | cmd.Stderr = os.Stderr 276 | 277 | err = cmd.Run() 278 | if err != nil { 279 | log.Fatalf("running '%s' failed: %s", strings.Join(c.Args(), " "), err) 280 | } 281 | } 282 | 283 | func shellCommand(c *cli.Context) { 284 | // bunch shell (bunch exec $SHELL) 285 | 286 | shell := "/bin/bash" 287 | envShell := os.Getenv("SHELL") 288 | if envShell != "" { 289 | shell = envShell 290 | } 291 | 292 | err := setVendorEnv() 293 | if err != nil { 294 | log.Fatalf("unable to set vendor env: %s", err) 295 | } 296 | 297 | fmt.Printf("starting bunch shell (%s)\n", shell) 298 | 299 | cmd := exec.Command(shell) 300 | cmd.Stdin = os.Stdin 301 | cmd.Stdout = os.Stdout 302 | cmd.Stderr = os.Stderr 303 | 304 | err = cmd.Run() 305 | if err != nil { 306 | log.Fatalf("running '%s' failed: %s", shell, err) 307 | } 308 | 309 | fmt.Println("exiting bunch shell") 310 | } 311 | 312 | var shimScript = `#!/bin/bash 313 | 314 | PATH=$(echo "$PATH" | sed -e "s|$HOME/.bunch/shims:||g") 315 | 316 | if [[ -n $(echo "$PATH" | grep .bunch/shims) ]]; then 317 | echo bunch warning: unable to remove shim from PATH, falling back to backup PATH 318 | PATH=/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/bin 319 | fi 320 | 321 | if [[ -f "Bunchfile" && -d ".vendor" ]]; then 322 | WD=$(pwd) 323 | PATH="$WD/.vendor/bin:$PATH" GOPATH="$WD/.vendor/" go $@ 324 | else 325 | go $@ 326 | fi 327 | ` 328 | 329 | func shimCommand(c *cli.Context) { 330 | // bunch shim outputs shell script 331 | 332 | err := os.MkdirAll(path.Join(os.Getenv("HOME"), ".bunch", "shims"), 0755) 333 | if err != nil { 334 | log.Fatalf("unable to create ~/.bunch") 335 | } 336 | 337 | goShimDir := path.Join(os.Getenv("HOME"), ".bunch", "shims") 338 | goShimPath := path.Join(goShimDir, "go") 339 | err = ioutil.WriteFile(goShimPath, []byte(shimScript), 0755) 340 | if err != nil { 341 | log.Fatalf("unable to create shim") 342 | } 343 | 344 | if len(c.Args()) > 0 { 345 | fmt.Printf("export PATH=%s:$PATH\n", goShimDir) 346 | } else { 347 | fmt.Println(`To have 'go' be automatically bunch-aware, add this to .bash_profile or .zshrc: 348 | 349 | if which bunch > /dev/null; then eval "$(bunch shim -)"; fi`) 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/juju/errors" 9 | ) 10 | 11 | func setVendorEnv() error { 12 | dir, err := os.Getwd() 13 | 14 | if err != nil { 15 | return errors.Trace(err) 16 | } 17 | 18 | newGoPath := path.Join(dir, ".vendor") 19 | newPath := fmt.Sprintf("%s:%s", path.Join(dir, ".vendor", "bin"), InitialPath) 20 | 21 | err = os.Setenv("PATH", newPath) 22 | if err != nil { 23 | return errors.Trace(err) 24 | } 25 | 26 | err = os.Setenv("GOPATH", newGoPath) 27 | if err != nil { 28 | return errors.Trace(err) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func unsetVendorEnv() error { 35 | err := os.Setenv("PATH", InitialPath) 36 | if err != nil { 37 | return errors.Trace(err) 38 | } 39 | 40 | err = os.Setenv("GOPATH", InitialGoPath) 41 | if err != nil { 42 | return errors.Trace(err) 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /packages.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | 14 | "github.com/briandowns/spinner" 15 | "github.com/fatih/color" 16 | "github.com/juju/errors" 17 | ) 18 | 19 | func pathExists(path string) (bool, error) { 20 | _, err := os.Stat(path) 21 | if err == nil { 22 | return true, nil 23 | } 24 | if os.IsNotExist(err) { 25 | return false, nil 26 | } 27 | return false, errors.Trace(err) 28 | } 29 | 30 | func getPackageRootDir(repo string) (string, error) { // move backwards through the package name, looking for a .git/.hg dir to find the package "root" 31 | gopath := os.Getenv("GOPATH") 32 | resultPath := path.Join(gopath, "src", repo) 33 | 34 | parts := strings.Split(repo, "/") 35 | for i := len(parts) - 1; i >= 0; i-- { 36 | repoPortion := path.Join(parts[:i]...) 37 | candidatePath := path.Join(gopath, "src", repoPortion) 38 | 39 | gitDir := path.Join(candidatePath, ".git") 40 | hgDir := path.Join(candidatePath, ".hg") 41 | bzrDir := path.Join(candidatePath, ".bzr") 42 | 43 | if exists, _ := pathExists(gitDir); exists { 44 | resultPath = candidatePath 45 | break 46 | } 47 | 48 | if exists, _ := pathExists(hgDir); exists { 49 | resultPath = candidatePath 50 | break 51 | } 52 | 53 | if exists, _ := pathExists(bzrDir); exists { 54 | resultPath = candidatePath 55 | break 56 | } 57 | } 58 | 59 | return resultPath, nil 60 | } 61 | 62 | func fetchPackage(repo string) error { 63 | wd, err := os.Getwd() 64 | if err != nil { 65 | return errors.Trace(err) 66 | } 67 | 68 | gopath := os.Getenv("GOPATH") 69 | packageDir := path.Join(gopath, "src", getRealRepoPath(repo)) 70 | 71 | if _, err := os.Stat(packageDir); err != nil { 72 | if os.IsNotExist(err) { 73 | var s *spinner.Spinner 74 | if Verbose { 75 | s = spinner.New(spinner.CharSets[SpinnerCharSet], SpinnerInterval) 76 | s.Prefix = fmt.Sprintf("fetching %s ", repo) 77 | s.Color("green") 78 | s.Start() 79 | } 80 | 81 | goGetCommand := []string{"go", "get", "-d", repo} 82 | goGetCmd := exec.Command(goGetCommand[0], goGetCommand[1:]...) 83 | err := goGetCmd.Run() 84 | 85 | if Verbose { 86 | s.Stop() 87 | } 88 | 89 | if err != nil { 90 | return errors.Annotatef(err, "failed cloning repo for package %s", repo) 91 | } else { 92 | if Verbose { 93 | fmt.Printf("\rfetching %s ... %s\n", repo, color.GreenString("done")) 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | } 100 | 101 | defer func() { 102 | _ = os.Chdir(wd) 103 | }() 104 | 105 | packageDir, err = getPackageRootDir(getRealRepoPath(repo)) 106 | if err != nil { 107 | return errors.Trace(err) 108 | } 109 | 110 | err = os.Chdir(packageDir) 111 | if err != nil { 112 | return errors.Trace(err) 113 | } 114 | 115 | var s *spinner.Spinner 116 | if Verbose { 117 | s = spinner.New(spinner.CharSets[SpinnerCharSet], SpinnerInterval) 118 | s.Prefix = fmt.Sprintf("refreshing %s ", repo) 119 | s.Color("green") 120 | s.Start() 121 | } 122 | 123 | var refreshCommand []string 124 | 125 | if exists, _ := pathExists(path.Join(packageDir, ".git")); exists { 126 | refreshCommand = []string{"git", "fetch", "--all"} 127 | } else if exists, _ := pathExists(path.Join(packageDir, ".hg")); exists { 128 | refreshCommand = []string{"hg", "pull"} 129 | } else if exists, _ := pathExists(path.Join(packageDir, ".bzr")); exists { 130 | refreshCommand = []string{"bzr", "pull"} 131 | } else if exists, _ := pathExists(path.Join(packageDir, ".svn")); exists { 132 | refreshCommand = []string{"svn", "up"} 133 | } 134 | 135 | if len(refreshCommand) > 0 { 136 | refreshOutput, err := exec.Command(refreshCommand[0], refreshCommand[1:]...).CombinedOutput() 137 | 138 | if Verbose { 139 | s.Stop() 140 | fmt.Printf("\rrefreshing %s ... %s\n", repo, color.GreenString("done")) 141 | } 142 | 143 | if err != nil { 144 | return errors.Annotatef(err, "failed updating repo for package %s, output: %s", repo, refreshOutput) 145 | } 146 | } else { 147 | if Verbose { 148 | s.Stop() 149 | fmt.Printf("\rrefreshing %s ... %s\n", repo, color.YellowString("skipped")) 150 | } 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func fetchPackageDependencies(repo string) error { 157 | wd, err := os.Getwd() 158 | if err != nil { 159 | return errors.Trace(err) 160 | } 161 | 162 | gopath := os.Getenv("GOPATH") 163 | packageDir := path.Join(gopath, "src", getRealRepoPath(repo)) 164 | 165 | defer func() { 166 | _ = os.Chdir(wd) 167 | }() 168 | 169 | err = os.Chdir(packageDir) 170 | if err != nil { 171 | return errors.Trace(err) 172 | } 173 | 174 | var s *spinner.Spinner 175 | 176 | if Verbose { 177 | s = spinner.New(spinner.CharSets[SpinnerCharSet], SpinnerInterval) 178 | s.Prefix = fmt.Sprintf(" - fetching dependencies for %s ", repo) 179 | s.Color("green") 180 | s.Start() 181 | } 182 | 183 | goGetCommand := []string{"go", "get", "-u", "-d", "./..."} 184 | goGetOutput, err := exec.Command(goGetCommand[0], goGetCommand[1:]...).CombinedOutput() 185 | 186 | if Verbose { 187 | s.Stop() 188 | fmt.Printf("\r - fetching dependencies for %s ... %s\n", repo, color.GreenString("done")) 189 | } 190 | 191 | if err != nil { 192 | return errors.Annotatef(err, "failed fetching dependencies for package %s, output: %s", repo, goGetOutput) 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func buildPackage(repo string) error { 199 | wd, err := os.Getwd() 200 | if err != nil { 201 | return errors.Trace(err) 202 | } 203 | 204 | gopath := os.Getenv("GOPATH") 205 | packageDir := path.Join(gopath, "src", getRealRepoPath(repo)) 206 | 207 | defer func() { 208 | _ = os.Chdir(wd) 209 | }() 210 | 211 | err = os.Chdir(packageDir) 212 | if err != nil { 213 | return errors.Trace(err) 214 | } 215 | 216 | var s *spinner.Spinner 217 | 218 | if Verbose { 219 | s = spinner.New(spinner.CharSets[SpinnerCharSet], SpinnerInterval) 220 | s.Prefix = fmt.Sprintf(" - building package %s ", repo) 221 | s.Color("green") 222 | s.Start() 223 | } 224 | 225 | goBuildCommand := []string{"go", "build", repo} 226 | goBuildOutput, err := exec.Command(goBuildCommand[0], goBuildCommand[1:]...).CombinedOutput() 227 | 228 | if Verbose { 229 | s.Stop() 230 | fmt.Printf("\r - building package %s ... %s\n", repo, color.GreenString("done")) 231 | } 232 | 233 | if err != nil { 234 | return errors.Annotatef(err, "failed building package %s, error: %s, output: %s", repo, goBuildOutput) 235 | } 236 | 237 | return nil 238 | } 239 | 240 | func installPackage(repo string) error { 241 | wd, err := os.Getwd() 242 | if err != nil { 243 | return errors.Trace(err) 244 | } 245 | 246 | gopath := os.Getenv("GOPATH") 247 | packageDir := path.Join(gopath, "src", getRealRepoPath(repo)) 248 | 249 | defer func() { 250 | _ = os.Chdir(wd) 251 | }() 252 | 253 | err = os.Chdir(packageDir) 254 | if err != nil { 255 | return errors.Trace(err) 256 | } 257 | 258 | var s *spinner.Spinner 259 | 260 | if Verbose { 261 | s = spinner.New(spinner.CharSets[SpinnerCharSet], SpinnerInterval) 262 | s.Prefix = fmt.Sprintf(" - installing package %s ", repo) 263 | s.Color("green") 264 | s.Start() 265 | } 266 | 267 | goInstallCommand := []string{"go", "install", repo} 268 | goInstallOutput, err := exec.Command(goInstallCommand[0], goInstallCommand[1:]...).CombinedOutput() 269 | 270 | if Verbose { 271 | s.Stop() 272 | fmt.Printf("\r - installing package %s ... %s\n", repo, color.GreenString("done")) 273 | } 274 | 275 | if err != nil { 276 | return errors.Annotatef(err, "failed installing package %s, error: %s, output: %s", repo, goInstallOutput) 277 | } 278 | 279 | return nil 280 | } 281 | 282 | func setPackageVersion(repo string, version string, humanVersion string) error { 283 | if version == "" { 284 | return nil 285 | } 286 | 287 | wd, err := os.Getwd() 288 | if err != nil { 289 | return errors.Trace(err) 290 | } 291 | 292 | packageDir, err := getPackageRootDir(getRealRepoPath(repo)) 293 | if err != nil { 294 | return errors.Trace(err) 295 | } 296 | 297 | defer func() { 298 | _ = os.Chdir(wd) 299 | }() 300 | 301 | err = os.Chdir(packageDir) 302 | if err != nil { 303 | return errors.Trace(err) 304 | } 305 | 306 | var checkoutCommand []string 307 | if exists, _ := pathExists(".git"); exists { 308 | checkoutCommand = []string{"git", "checkout", version} 309 | } else if exists, _ := pathExists(".hg"); exists { 310 | checkoutCommand = []string{"hg", "update", "-c", version} 311 | } else if exists, _ := pathExists(".bzr"); exists { 312 | if version != "" { 313 | checkoutCommand = []string{"bzr", "update", "-r", version} 314 | } else { 315 | checkoutCommand = []string{"bzr", "update"} 316 | } 317 | } else { 318 | if Verbose { 319 | fmt.Printf(" - setting version of %s to %s (resolved as %s) ... %s\n", repo, humanVersion, version, color.GreenString("skipped, unknown repo type")) 320 | } 321 | return nil 322 | } 323 | 324 | var s *spinner.Spinner 325 | 326 | if Verbose { 327 | s = spinner.New(spinner.CharSets[SpinnerCharSet], SpinnerInterval) 328 | s.Prefix = fmt.Sprintf(" - setting version of %s to %s (resolved as %s) ", repo, humanVersion, version) 329 | s.Color("green") 330 | s.Start() 331 | } 332 | 333 | checkoutOutput, err := exec.Command(checkoutCommand[0], checkoutCommand[1:]...).CombinedOutput() 334 | 335 | if Verbose { 336 | s.Stop() 337 | fmt.Printf("\r - setting version of %s to %s (resolved as %s) ... %s\n", repo, humanVersion, version, color.GreenString("done")) 338 | } 339 | 340 | if err != nil { 341 | return errors.Annotatef(err, "failed setting version of package %s, error: %s, output: %s", repo, checkoutOutput) 342 | } 343 | 344 | return nil 345 | } 346 | 347 | func countNonEmptyStrings(ar []string) int { 348 | counter := 0 349 | 350 | for _, el := range ar { 351 | if el != "" { 352 | counter += 1 353 | } 354 | } 355 | 356 | return counter 357 | } 358 | 359 | func getRealRepoPath(pack string) string { 360 | if strings.HasSuffix(pack, "...") { 361 | return strings.Replace(pack, "/...", "", -1) 362 | } else { 363 | return pack 364 | } 365 | } 366 | 367 | type PackageRecencyInfo struct { 368 | LatestCommit string 369 | LatestUpstreamCommit string 370 | InstalledCommit string 371 | UpstreamDiffCount int 372 | InstalledDiffCount int 373 | } 374 | 375 | func checkPackageRecency(pack Package) (bool, PackageRecencyInfo, error) { // bool = needsUpdate 376 | NilInfo := PackageRecencyInfo{} 377 | 378 | repo := getRealRepoPath(pack.Repo) 379 | version, err := getLatestVersionMatchingPattern(pack.Repo, pack.Version) 380 | 381 | if err != nil { 382 | return false, NilInfo, errors.Trace(err) 383 | } 384 | 385 | wd, err := os.Getwd() 386 | if err != nil { 387 | return false, NilInfo, errors.Trace(err) 388 | } 389 | 390 | gopath := os.Getenv("GOPATH") 391 | packageDir := path.Join(gopath, "src", repo) 392 | 393 | if exists, _ := pathExists(packageDir); !exists { 394 | return true, NilInfo, nil 395 | } 396 | 397 | packageDir, err = getPackageRootDir(repo) 398 | if err != nil { 399 | return false, NilInfo, errors.Trace(err) 400 | } 401 | 402 | defer func() { 403 | _ = os.Chdir(wd) 404 | }() 405 | 406 | err = os.Chdir(packageDir) 407 | if err != nil { 408 | return false, NilInfo, errors.Trace(err) 409 | } 410 | 411 | var repoType string 412 | 413 | if exists, _ := pathExists(".git"); exists { 414 | repoType = "git" 415 | } else if exists, _ := pathExists(".hg"); exists { 416 | repoType = "hg" 417 | } else { 418 | return true, NilInfo, nil // force an update 419 | } 420 | 421 | var getVersionCommand, getHEADCommand, getUpstreamVersionCommand, getUpstreamDiffCommand, getInstalledDiffCommand []string 422 | 423 | if repoType == "git" { 424 | getVersionCommand = []string{"git", "rev-parse", "-q", "--verify", version} 425 | getHEADCommand = []string{"git", "rev-parse", "-q", "--verify", "HEAD"} 426 | getUpstreamVersionCommand = []string{"git", "rev-parse", "-q", "--verify", "origin/master"} 427 | getUpstreamDiffCommand = []string{"git", "log", "HEAD..origin/master", "--pretty=oneline"} 428 | getInstalledDiffCommand = []string{"git", "log", fmt.Sprintf("HEAD..%s", version), "--pretty=oneline"} 429 | } else if repoType == "hg" { 430 | getVersionCommand = []string{"hg", "identify", "-ir", version} 431 | getHEADCommand = []string{"hg", "identify", "-i"} 432 | getUpstreamVersionCommand = []string{"hg", "identify", "-ir", "tip"} // imperfect, but there's no git equivalent to this 433 | getUpstreamDiffCommand = []string{"echo"} 434 | getInstalledDiffCommand = []string{"echo"} // can't really even approximate this 435 | } 436 | 437 | getVersionOutput, err := exec.Command(getVersionCommand[0], getVersionCommand[1:]...).Output() 438 | if err != nil { 439 | return false, NilInfo, errors.Trace(err) 440 | } 441 | 442 | getUpstreamVersionOutput, err := exec.Command(getUpstreamVersionCommand[0], getUpstreamVersionCommand[1:]...).Output() 443 | if err != nil { 444 | return false, NilInfo, errors.Trace(err) 445 | } 446 | 447 | getHEADOutput, err := exec.Command(getHEADCommand[0], getHEADCommand[1:]...).Output() 448 | if err != nil { 449 | return false, NilInfo, errors.Trace(err) 450 | } 451 | 452 | upstreamDiffCount := 0 453 | getUpstreamDiffOutput, err := exec.Command(getUpstreamDiffCommand[0], getUpstreamDiffCommand[1:]...).CombinedOutput() 454 | if err == nil { 455 | upstreamDiffCount = countNonEmptyStrings(strings.Split(strings.TrimSpace(string(getUpstreamDiffOutput)), "\n")) 456 | } 457 | 458 | installedDiffCount := 0 459 | getInstalledDiffOutput, err := exec.Command(getInstalledDiffCommand[0], getInstalledDiffCommand[1:]...).CombinedOutput() 460 | if err == nil { 461 | installedDiffCount = countNonEmptyStrings(strings.Split(strings.TrimSpace(string(getInstalledDiffOutput)), "\n")) 462 | } 463 | 464 | versionString := strings.TrimSpace(string(getVersionOutput)) 465 | HEADString := strings.TrimSpace(string(getHEADOutput)) 466 | upstreamVersionString := strings.TrimSpace(string(getUpstreamVersionOutput)) 467 | 468 | recencyInfo := PackageRecencyInfo{ 469 | LatestCommit: versionString, 470 | LatestUpstreamCommit: upstreamVersionString, 471 | InstalledCommit: HEADString, 472 | UpstreamDiffCount: upstreamDiffCount, 473 | InstalledDiffCount: installedDiffCount, 474 | } 475 | 476 | pkgPath := fmt.Sprintf("%s.a", path.Join(gopath, "pkg", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH), repo)) 477 | 478 | if exists, _ := pathExists(pkgPath); !exists { 479 | return true, recencyInfo, nil 480 | } 481 | 482 | if versionString != HEADString { 483 | if pack.LockedVersion != HEADString { 484 | if version == "" { 485 | return false, recencyInfo, nil 486 | } else { 487 | return true, recencyInfo, nil 488 | } 489 | } else { 490 | return false, recencyInfo, nil 491 | } 492 | } else { 493 | if pack.LockedVersion != "" && pack.LockedVersion != HEADString { 494 | return true, recencyInfo, nil 495 | } else { 496 | return false, recencyInfo, nil 497 | } 498 | } 499 | 500 | return false, NilInfo, nil 501 | } 502 | 503 | func parsePackage(packString string) Package { 504 | parts := strings.Split(packString, "@") 505 | pack := Package{} 506 | 507 | if len(parts) == 2 { 508 | pack.Repo = parts[0] 509 | pack.Version = parts[1] 510 | } else { 511 | pack.Repo = parts[0] 512 | } 513 | 514 | repoParts := strings.Split(pack.Repo, "/") 515 | if len(repoParts) == 2 { 516 | if !strings.Contains(repoParts[0], ".") { 517 | // github shorthand 518 | pack.Repo = fmt.Sprintf("github.com/%s", pack.Repo) 519 | } 520 | } 521 | 522 | return pack 523 | } 524 | 525 | func installPackagesFromBunchfile(b *BunchFile, forceUpdate bool, checkUpstream bool, respectLocked bool) error { 526 | return installPackages(b.Packages, false, forceUpdate, checkUpstream, respectLocked) 527 | } 528 | 529 | func installPackagesFromRepoStrings(packageStrings []string, installGlobally bool, forceUpdate bool, checkUpstream bool, respectLocked bool) error { 530 | packages := make([]Package, len(packageStrings)) 531 | for i, packString := range packageStrings { 532 | packages[i] = parsePackage(packString) 533 | } 534 | 535 | return installPackages(packages, installGlobally, forceUpdate, checkUpstream, respectLocked) 536 | } 537 | 538 | func installPackages(packages []Package, installGlobally bool, forceUpdate bool, checkUpstream bool, respectLocked bool) error { 539 | if !installGlobally { 540 | err := setVendorEnv() 541 | if err != nil { 542 | return errors.Trace(err) 543 | } 544 | } 545 | 546 | gopath := os.Getenv("GOPATH") 547 | 548 | anyNeededUpdate := false 549 | packageNeedsUpdate := make(map[string]bool) 550 | 551 | for _, pack := range packages { 552 | if pack.IsLink { 553 | repoPath := path.Join(gopath, "src", pack.Repo) 554 | 555 | if exists, _ := pathExists(repoPath); !exists { 556 | err := os.MkdirAll(filepath.Dir(repoPath), 0755) 557 | if err != nil { 558 | return errors.Trace(err) 559 | } 560 | 561 | err = os.Symlink(pack.LinkTarget, path.Join(gopath, "src", pack.Repo)) 562 | if err != nil { 563 | return errors.Trace(err) 564 | } 565 | 566 | if !pack.IsSelf { 567 | fmt.Printf("\rsetting up local package %s ... %s \n", pack.Repo, color.GreenString("done")) 568 | } else { 569 | fmt.Printf("\rsetting up %s link ... %s \n", pack.Repo, color.GreenString("done")) 570 | } 571 | } 572 | 573 | continue 574 | } 575 | 576 | needsUpdate, _, err := checkPackageRecency(pack) 577 | if err != nil { 578 | return errors.Trace(err) 579 | } 580 | 581 | if needsUpdate { 582 | packageNeedsUpdate[pack.Repo] = true 583 | anyNeededUpdate = true 584 | } 585 | 586 | if (needsUpdate || forceUpdate) && checkUpstream { 587 | if !Verbose { 588 | fmt.Printf("fetching %s ... ", pack.Repo) 589 | } 590 | 591 | err = fetchPackage(pack.Repo) 592 | if err != nil { 593 | return errors.Trace(err) 594 | } 595 | 596 | err = fetchPackageDependencies(pack.Repo) 597 | if err != nil { 598 | return errors.Trace(err) 599 | } 600 | 601 | if Verbose { 602 | fmt.Println("") 603 | } else { 604 | fmt.Printf("\rfetching %s ... %s \n", pack.Repo, color.GreenString("done")) 605 | } 606 | } 607 | } 608 | 609 | for _, pack := range packages { 610 | needsUpdate := packageNeedsUpdate[pack.Repo] 611 | 612 | if needsUpdate || forceUpdate { 613 | if Verbose { 614 | fmt.Printf("installing %s ... \n", pack.Repo) 615 | } else { 616 | fmt.Printf("installing %s ... ", pack.Repo) 617 | } 618 | 619 | version := pack.Version 620 | 621 | if !pack.IsLink { 622 | var err error 623 | version, err = getLatestVersionMatchingPattern(pack.Repo, pack.Version) 624 | if err != nil { 625 | return errors.Trace(err) 626 | } 627 | } 628 | 629 | if pack.LockedVersion != "" && respectLocked { 630 | version = pack.LockedVersion 631 | } 632 | 633 | if !pack.IsLink { 634 | err := setPackageVersion(pack.Repo, version, pack.Version) 635 | if err != nil { 636 | return errors.Trace(err) 637 | } 638 | } 639 | 640 | if !pack.IsSelf { 641 | err := buildPackage(pack.Repo) 642 | if err != nil { 643 | return errors.Trace(err) 644 | } 645 | 646 | err = installPackage(pack.Repo) 647 | if err != nil { 648 | return errors.Trace(err) 649 | } 650 | } 651 | 652 | if Verbose { 653 | fmt.Print(color.GreenString("\rsuccessfully installed %s \n\n", pack.Repo)) 654 | } else { 655 | fmt.Printf("\rinstalling %s ... %s \n", pack.Repo, color.GreenString("done")) 656 | } 657 | 658 | } else { 659 | if Verbose { 660 | fmt.Print(color.YellowString("skipping %s, up to date \n", pack.Repo)) 661 | } 662 | } 663 | } 664 | 665 | if !anyNeededUpdate && !Verbose && !forceUpdate { 666 | color.Green("up to date (use 'bunch update' to force update)") 667 | } 668 | 669 | return nil 670 | } 671 | 672 | type GoList struct { 673 | Name string 674 | Doc string 675 | ImportPath string 676 | Imports []string 677 | TestImports []string 678 | Deps []string 679 | } 680 | 681 | func isEmptyDir(name string) (bool, error) { 682 | entries, err := ioutil.ReadDir(name) 683 | if err != nil { 684 | return false, errors.Trace(err) 685 | } 686 | return len(entries) == 0, nil 687 | } 688 | 689 | func cleanEmpties(emptyPath string) error { 690 | higherPath := filepath.Dir(emptyPath) 691 | if empty, _ := isEmptyDir(higherPath); empty { 692 | err := os.Remove(higherPath) 693 | if err != nil { 694 | return errors.Trace(err) 695 | } 696 | 697 | highestPath := filepath.Dir(higherPath) 698 | if empty, _ := isEmptyDir(highestPath); empty { 699 | err := os.Remove(highestPath) 700 | if err != nil { 701 | return errors.Trace(err) 702 | } 703 | } 704 | } 705 | 706 | return nil 707 | } 708 | 709 | func removePackage(pack string) error { 710 | gopath := os.Getenv("GOPATH") 711 | 712 | srcPath := path.Join(gopath, "src", pack) 713 | if exists, _ := pathExists(srcPath); exists { 714 | err := os.RemoveAll(srcPath) 715 | if err != nil { 716 | return errors.Trace(err) 717 | } 718 | } 719 | 720 | err := cleanEmpties(srcPath) 721 | if err != nil { 722 | return errors.Trace(err) 723 | } 724 | 725 | archPath := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH) 726 | pkgPath := fmt.Sprintf("%s.a", path.Join(gopath, "pkg", archPath, pack)) 727 | if exists, _ := pathExists(pkgPath); exists { 728 | err := os.Remove(pkgPath) 729 | if err != nil { 730 | return errors.Trace(err) 731 | } 732 | } 733 | 734 | err = cleanEmpties(pkgPath) 735 | if err != nil { 736 | return errors.Trace(err) 737 | } 738 | 739 | _, binFile := path.Split(pack) 740 | 741 | if binFile != "" { 742 | binPath := path.Join(gopath, "bin", binFile) 743 | if exists, _ := pathExists(binPath); exists { 744 | err := os.Remove(binPath) 745 | if err != nil { 746 | return errors.Trace(err) 747 | } 748 | } 749 | } 750 | 751 | return nil 752 | } 753 | 754 | func removePackages(packages []string, bunch *BunchFile, removeGlobally bool) error { 755 | if !removeGlobally { 756 | err := setVendorEnv() 757 | if err != nil { 758 | return errors.Trace(err) 759 | } 760 | } 761 | 762 | gopath := os.Getenv("GOPATH") 763 | 764 | allPackages := make(map[string]bool) 765 | packagesUsed := make(map[string][]string) 766 | 767 | combinedPackagesList := packages 768 | for _, packData := range bunch.Packages { 769 | combinedPackagesList = append(combinedPackagesList, packData.Repo) 770 | } 771 | 772 | for _, pack := range combinedPackagesList { 773 | if exists, _ := pathExists(path.Join(gopath, "src", pack)); !exists { 774 | continue 775 | } 776 | 777 | goListCommand := []string{"go", "list", "--json", pack} 778 | output, err := exec.Command(goListCommand[0], goListCommand[1:]...).Output() 779 | if err != nil { 780 | return errors.Trace(err) 781 | } 782 | 783 | packageInfo := GoList{} 784 | err = json.Unmarshal(output, &packageInfo) 785 | 786 | if err != nil { 787 | return errors.Trace(err) 788 | } 789 | 790 | removingPackage := false 791 | for _, removePack := range packages { 792 | if removePack == pack { 793 | removingPackage = true 794 | } 795 | } 796 | 797 | if !removingPackage { 798 | packagesUsed[pack] = append(packagesUsed[pack], "app") 799 | } 800 | 801 | for _, dep := range packageInfo.Deps { 802 | srcPath := path.Join(gopath, "src", dep) 803 | if exists, _ := pathExists(srcPath); exists { 804 | allPackages[dep] = true 805 | 806 | if !removingPackage { 807 | packagesUsed[dep] = append(packagesUsed[dep], pack) 808 | } 809 | } 810 | } 811 | 812 | allPackages[pack] = true 813 | } 814 | 815 | for _, pack := range packages { 816 | if len(packagesUsed[pack]) > 0 { 817 | color.Red("unable to remove package %s, is depended on by %s", pack, strings.Join(packagesUsed[pack], ", ")) 818 | } 819 | } 820 | 821 | for pack, _ := range allPackages { 822 | if len(packagesUsed[pack]) == 0 { 823 | fmt.Printf("removing package %s ...", pack) 824 | err := removePackage(pack) 825 | 826 | if err != nil { 827 | return errors.Trace(err) 828 | } 829 | 830 | fmt.Printf("\rremoving package %s ... %s \n", pack, color.GreenString("done")) 831 | } 832 | } 833 | 834 | return nil 835 | } 836 | 837 | func isRootPackageUsed(packagesUsed map[string]bool, packName string) bool { 838 | for pack, _ := range packagesUsed { 839 | if strings.HasPrefix(pack, packName) { 840 | return true 841 | } 842 | } 843 | 844 | return false 845 | } 846 | 847 | func prunePackages(bunch *BunchFile) error { 848 | err := setVendorEnv() 849 | if err != nil { 850 | return errors.Trace(err) 851 | } 852 | 853 | gopath := os.Getenv("GOPATH") 854 | 855 | packagesUsed := make(map[string]bool) 856 | 857 | for _, packInfo := range bunch.Packages { 858 | pack := packInfo.Repo 859 | 860 | if exists, _ := pathExists(path.Join(gopath, "src", pack)); !exists { 861 | continue 862 | } 863 | 864 | goListCommand := []string{"go", "list", "--json", pack} 865 | output, err := exec.Command(goListCommand[0], goListCommand[1:]...).Output() 866 | if err != nil { 867 | return errors.Trace(err) 868 | } 869 | 870 | packageInfo := GoList{} 871 | err = json.Unmarshal(output, &packageInfo) 872 | 873 | if err != nil { 874 | return errors.Trace(err) 875 | } 876 | 877 | packagesUsed[pack] = true 878 | 879 | for _, dep := range packageInfo.Deps { 880 | srcPath := path.Join(gopath, "src", dep) 881 | if exists, _ := pathExists(srcPath); exists { 882 | packagesUsed[dep] = true 883 | } 884 | } 885 | } 886 | 887 | wd, err := os.Getwd() 888 | if err != nil { 889 | return errors.Trace(err) 890 | } 891 | 892 | err = os.Chdir(path.Join(gopath, "src")) 893 | if err != nil { 894 | return errors.Trace(err) 895 | } 896 | 897 | packFiles := []string{} 898 | err = filepath.Walk(".", func(packPath string, info os.FileInfo, err error) error { 899 | if err != nil { 900 | return err 901 | } 902 | 903 | gitExists, _ := pathExists(path.Join(packPath, ".git")) 904 | hgExists, _ := pathExists(path.Join(packPath, ".hg")) 905 | bzrExists, _ := pathExists(path.Join(packPath, ".bzr")) 906 | 907 | if gitExists || hgExists || bzrExists { 908 | packFiles = append(packFiles, packPath) 909 | } 910 | 911 | return nil 912 | }) 913 | if err != nil { 914 | return errors.Trace(err) 915 | } 916 | 917 | err = os.Chdir(wd) 918 | if err != nil { 919 | return errors.Trace(err) 920 | } 921 | 922 | for _, pack := range packFiles { 923 | if !packagesUsed[pack] && !isRootPackageUsed(packagesUsed, pack) { 924 | fmt.Printf("removing package %s ...", pack) 925 | err := removePackage(pack) 926 | 927 | if err != nil { 928 | return errors.Trace(err) 929 | } 930 | 931 | fmt.Printf("\rremoving package %s ... %s \n", pack, color.GreenString("done")) 932 | } 933 | } 934 | 935 | return nil 936 | } 937 | 938 | func gitShort(fullhash string) string { 939 | if len(fullhash) < 8 { 940 | return fullhash 941 | } else { 942 | return fullhash[:7] 943 | } 944 | } 945 | 946 | func commitsPlural(n int) string { 947 | if n == 1 { 948 | return "1 commit" 949 | } else { 950 | return fmt.Sprintf("%d commits", n) 951 | } 952 | } 953 | 954 | func checkOutdatedPackages(b *BunchFile) error { 955 | err := setVendorEnv() 956 | if err != nil { 957 | return errors.Trace(err) 958 | } 959 | 960 | for _, pack := range b.Packages { 961 | if pack.IsSelf { 962 | continue 963 | } 964 | 965 | fmt.Printf("package %s ... ", pack.Repo) 966 | 967 | err := fetchPackage(pack.Repo) 968 | if err != nil { 969 | return errors.Trace(err) 970 | } 971 | 972 | needsUpdate, recency, err := checkPackageRecency(pack) 973 | if err != nil { 974 | return errors.Trace(err) 975 | } 976 | 977 | if !needsUpdate { 978 | if recency.UpstreamDiffCount == 0 { 979 | fmt.Printf("\rpackage %s ... %s\n", pack.Repo, color.GreenString("up to date")) 980 | } else { 981 | if pack.LockedVersion == "" { 982 | fmt.Printf("\rpackage %s ... %s by %s, current is %6s, latest is %6s\n", pack.Repo, color.YellowString("behind upstream"), commitsPlural(recency.UpstreamDiffCount), gitShort(recency.InstalledCommit), gitShort(recency.LatestUpstreamCommit)) 983 | } else { 984 | fmt.Printf("\rpackage %s ... %s by %s, current is %6s, latest is %6s\n", pack.Repo, color.YellowString("locked, but behind upstream"), commitsPlural(recency.UpstreamDiffCount), gitShort(recency.InstalledCommit), gitShort(recency.LatestUpstreamCommit)) 985 | } 986 | } 987 | } else { 988 | if recency.InstalledDiffCount == 0 { 989 | fmt.Printf("\rpackage %s ... %s\n", pack.Repo, color.GreenString("up to date")) 990 | } else { 991 | if pack.LockedVersion == "" { 992 | fmt.Printf("\rpackage %s ... %s by %s, current is %6s, latest is %6s\n", pack.Repo, color.RedString("outdated"), commitsPlural(recency.InstalledDiffCount), gitShort(recency.InstalledCommit), gitShort(recency.LatestCommit)) 993 | } else { 994 | fmt.Printf("\rpackage %s ... %s by %s, current is %6s, latest is %6s\n", pack.Repo, color.YellowString("locked, but outdated"), commitsPlural(recency.InstalledDiffCount), gitShort(recency.InstalledCommit), gitShort(recency.LatestCommit)) 995 | } 996 | } 997 | } 998 | } 999 | 1000 | return nil 1001 | } 1002 | 1003 | func lockPackages(b *BunchFile) error { 1004 | err := setVendorEnv() 1005 | if err != nil { 1006 | return errors.Trace(err) 1007 | } 1008 | 1009 | lockList := make(map[string]string) 1010 | 1011 | for _, pack := range b.Packages { 1012 | if pack.IsLink { 1013 | continue 1014 | } 1015 | 1016 | _, recency, err := checkPackageRecency(pack) 1017 | if err != nil { 1018 | return errors.Trace(err) 1019 | } 1020 | 1021 | lockList[pack.Repo] = recency.LatestCommit 1022 | } 1023 | 1024 | jsonOut, err := json.MarshalIndent(lockList, "", " ") 1025 | if err != nil { 1026 | return errors.Trace(err) 1027 | } else { 1028 | err = ioutil.WriteFile("Bunchfile.lock", append(jsonOut, '\n'), 0644) 1029 | if err != nil { 1030 | return errors.Trace(err) 1031 | } 1032 | 1033 | color.Green("Bunchfile.lock generated successfully") 1034 | } 1035 | 1036 | return nil 1037 | } 1038 | -------------------------------------------------------------------------------- /packages_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPathExists(t *testing.T) { 10 | exists, err := pathExists("/") 11 | assert.Nil(t, err, "path exists should not error out") 12 | 13 | assert.Equal(t, exists, true, "/ path should exist") 14 | } 15 | 16 | func TestCountNonEmptyString(t *testing.T) { 17 | emptiesGalore := []string{"a", "b", "", "c", "", "", "d", ""} 18 | noEmpties := []string{"a", "b", "c"} 19 | 20 | assert.Equal(t, 4, countNonEmptyStrings(emptiesGalore), "should count 4 non-empties") 21 | assert.Equal(t, 3, countNonEmptyStrings(noEmpties), "should count 3 non-empties") 22 | } 23 | 24 | func TestParsePackage(t *testing.T) { 25 | pack1 := parsePackage("github.com/a/b/c") 26 | pack2 := parsePackage("github.com/a/b/c@v1.2.0") 27 | pack3 := parsePackage("a/b") 28 | pack4 := parsePackage("gopkg.in/abc") 29 | 30 | assert.Equal(t, pack1.Repo, "github.com/a/b/c", "package repo should equal") 31 | assert.Equal(t, pack2.Repo, "github.com/a/b/c", "package repo should equal") 32 | 33 | assert.Equal(t, pack1.Version, "", "package version should be unset") 34 | assert.Equal(t, pack2.Version, "v1.2.0", "package version should be set") 35 | 36 | assert.Equal(t, pack3.Repo, "github.com/a/b", "github shorthand should have been expanded") 37 | assert.Equal(t, pack4.Repo, "gopkg.in/abc", "package containing domain should not have been expanded") 38 | } 39 | 40 | /* 41 | pack1 := parsePackage("github.com/a/b/c") 42 | pack2 := parsePackage("github.com/a/b/c !self") 43 | pack3 := parsePackage("github.com/a/b/c !link:/tmp") 44 | pack4 := parsePackage("github.com/a/b/c v1.2.0") 45 | pack5 := parsePackage("github.com/a/b/c master") 46 | 47 | assert.Equal(t, pack1.Repo, "github.com/a/b/c", "package repo should equal") 48 | assert.Equal(t, pack2.Repo, "github.com/a/b/c", "package repo should equal") 49 | assert.Equal(t, pack3.Repo, "github.com/a/b/c", "package repo should equal") 50 | assert.Equal(t, pack4.Repo, "github.com/a/b/c", "package repo should equal") 51 | assert.Equal(t, pack5.Repo, "github.com/a/b/c", "package repo should equal") 52 | 53 | assert.Equal(t, pack1.Version, "", "package version should be unset") 54 | assert.Equal(t, pack4.Version, "v1.2.0", "package version should be set") 55 | assert.Equal(t, pack5.Version, "master", "package version should be set") 56 | 57 | assert.Equal(t, pack2.IsSelf, true, "package should be marked isself") 58 | assert.Equal(t, pack2.IsLink, true, "package should be marked islink") 59 | assert.Equal(t, pack3.IsLink, true, "package should be marked islink") 60 | assert.NotEqual(t, pack2.LinkTarget, "", "package should have link target set") 61 | assert.NotEqual(t, pack3.LinkTarget, "", "package should have link target set") 62 | */ 63 | -------------------------------------------------------------------------------- /versions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "sort" 8 | "strings" 9 | 10 | version "github.com/hashicorp/go-version" 11 | "github.com/juju/errors" 12 | ) 13 | 14 | func getLatestVersionMatchingPattern(repo string, versionPattern string) (string, error) { 15 | repoPath, err := getPackageRootDir(repo) 16 | if err != nil { 17 | return "", errors.Trace(err) 18 | } 19 | 20 | if exists, _ := pathExists(repoPath); !exists { 21 | return versionPattern, nil 22 | } 23 | 24 | wd, err := os.Getwd() 25 | if err != nil { 26 | return "", errors.Trace(err) 27 | } 28 | 29 | err = os.Chdir(repoPath) 30 | if err != nil { 31 | return "", errors.Trace(err) 32 | } 33 | 34 | defer func() { 35 | _ = os.Chdir(wd) 36 | }() 37 | 38 | var repoType string 39 | 40 | if exists, _ := pathExists(".git"); exists { 41 | repoType = "git" 42 | } else if exists, _ := pathExists(".hg"); exists { 43 | repoType = "hg" 44 | } else if exists, _ := pathExists(".bzr"); exists { 45 | repoType = "bzr" 46 | } else { 47 | return versionPattern, nil 48 | } 49 | 50 | if versionPattern == "" { 51 | if repoType == "git" { 52 | return "master", nil 53 | } else if repoType == "hg" { 54 | return "tip", nil 55 | } else if repoType == "bzr" { 56 | return "", nil 57 | } 58 | } 59 | 60 | if repoType == "hg" || repoType == "bzr" { 61 | return versionPattern, nil 62 | } 63 | 64 | // first, try feeding it through git to see if it's a valid rev 65 | gitResolveCommand := []string{"git", "rev-parse", "-q", "--verify", versionPattern} 66 | output, err := exec.Command(gitResolveCommand[0], gitResolveCommand[1:]...).Output() 67 | 68 | if err != nil { 69 | if _, ok := err.(*exec.ExitError); !ok { 70 | return "", errors.Trace(err) 71 | } 72 | } 73 | 74 | gitResolvedString := strings.TrimSpace(string(output)) 75 | 76 | if gitResolvedString != "" { 77 | return gitResolvedString, nil 78 | } 79 | 80 | // second, try parsing it 81 | tagListB, err := exec.Command("git", "tag").Output() 82 | if err != nil { 83 | return "", errors.Trace(err) 84 | } 85 | 86 | versionToTag := make(map[*version.Version]string) 87 | 88 | tagList := strings.Split(strings.TrimSpace(string(tagListB)), "\n") 89 | processedTagList := make([]*version.Version, len(tagList)) 90 | for i, tag := range tagList { 91 | stringVersion := tag 92 | 93 | if strings.HasPrefix(tag, "v") { 94 | stringVersion = strings.Replace(tag, "v", "", 1) 95 | } 96 | 97 | v, err := version.NewVersion(stringVersion) 98 | if err != nil { 99 | continue 100 | } 101 | 102 | processedTagList[i] = v 103 | versionToTag[v] = tag 104 | } 105 | 106 | sort.Sort(version.Collection(processedTagList)) 107 | 108 | constraints, err := version.NewConstraint(versionPattern) 109 | if err != nil { 110 | return "", errors.Trace(err) 111 | } 112 | 113 | var resultVersion string 114 | for i := len(processedTagList) - 1; i >= 0; i-- { 115 | ver := processedTagList[i] 116 | if constraints.Check(ver) { 117 | resultVersion = versionToTag[ver] 118 | } 119 | } 120 | 121 | if resultVersion == "" { 122 | return "", fmt.Errorf("unable to find a version matching constraint %s for package %s", versionPattern, repo) 123 | } 124 | 125 | gitResolveCommand = []string{"git", "rev-parse", "-q", "--verify", resultVersion} 126 | output, err = exec.Command(gitResolveCommand[0], gitResolveCommand[1:]...).Output() 127 | 128 | if err != nil { 129 | return "", errors.Trace(err) 130 | } else { 131 | return strings.TrimSpace(string(output)), nil 132 | } 133 | } 134 | --------------------------------------------------------------------------------