Repository: nitrous-io/goop Branch: master Commit: 78b83e479865 Files: 17 Total size: 29.5 KB Directory structure: gitextract__nt8qh5a/ ├── .gitignore ├── Goopfile ├── LICENSE ├── Makefile ├── README.md ├── colors/ │ └── colors.go ├── goop/ │ ├── goget.go │ ├── goop.go │ ├── goop_test.go │ ├── vcs.go │ └── vcs_test.go ├── main.go ├── parser/ │ ├── dependency.go │ ├── parser.go │ └── parser_test.go └── pkg/ └── env/ ├── env.go └── env_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .vendor # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so *.test # Folders _obj/ _test/ bin/ _vendor/ build/ # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.swp **.orig .env # OSX .DS_Store ._* .Spotlight-V100 .Trashes # Linux *~ .directory # Windows Thumbs.db Desktop.ini # RubyMine .idea/ # TextMate *.tmproj *.tmproject tmtags # Vim .*.sw[a-z] *.un~ Session.vim tags # Emacs \#*\# /.emacs.desktop /.emacs.desktop.lock .elc auto-save-list tramp .\#* # Vagrant .vagrant ================================================ FILE: Goopfile ================================================ github.com/onsi/ginkgo/ginkgo github.com/onsi/gomega code.google.com/p/go.tools/go/vcs ================================================ FILE: LICENSE ================================================ Copyright (c) 2014 Irrational Industries, Inc. d.b.a. Nitrous.IO. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ NO_COLOR=\033[0m OK_COLOR=\033[32;01m ERROR_COLOR=\033[31;01m WARN_COLOR=\033[33;01m BIN_NAME=goop all: build build: @mkdir -p build/ @echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)" go get -d -v ./... @echo "$(OK_COLOR)==> Building$(NO_COLOR)" go install -x ./... cp $(GOPATH)/bin/$(BIN_NAME) build/$(BIN_NAME) format: go fmt ./... test: @echo "$(OK_COLOR)==> Testing...$(NO_COLOR)" @go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs -n1 go get -d @goop exec ginkgo -r -trace -keepGoing .PHONY: all build format test ================================================ FILE: README.md ================================================ Goop ==== ![Goopie](https://raw.githubusercontent.com/nitrous-io/goop/master/goopie.png) A dependency manager for Go (golang), inspired by Bundler. It is different from other dependency managers in that it does not force you to mess with your `GOPATH`. ### Getting Started 1. Install Goop: `go get github.com/nitrous-io/goop` 2. Create `Goopfile`. Revision reference (e.g. Git SHA hash) is optional, but recommended. Prefix hash with `#`. (This is to futureproof the file format.) Example: ``` github.com/mattn/go-sqlite3 github.com/gorilla/context #14f550f51af52180c2eefed15e5fd18d63c0a64a github.com/dotcloud/docker/pkg/proxy #v1.0.1 // comment github.com/gorilla/mux !git@github.com:nitrous-io/mux.git // override repo url ``` 3. Run `goop install`. This will install packages inside a subdirectory called `.vendor` and create `Goopfile.lock`, recording exact versions used for each package and its dependencies. Subsequent `goop install` runs will ignore `Goopfile` and install the versions specified in `Goopfile.lock`. You should check this file in to your source version control. It's a good idea to add `.vendor` to your version control system's ignore settings (e.g. `.gitignore`). 4. Run commands using `goop exec` (e.g. `goop exec make`). This will execute your command in an environment that has correct `GOPATH` and `PATH` set. 5. Go commands can be run without the `exec` keyword (e.g. `goop go test`). ### Other commands * Run `goop update` to ignore an existing `Goopfile.lock`, and update to latest versions of packages (as specified in `Goopfile`). * Running `eval $(goop env)` will modify `GOPATH` and `PATH` in current shell session, allowing you to run commands without `goop exec`. ### Caveat Goop currently only supports Git and Mercurial. This should be fine for 99% of the cases, but you are more than welcome to make a pull request that adds support for Subversion and Bazaar. - - - [Work on awesome golang projects, like Goop, at Nitrous.IO](http://www.nitrous.io/jobs/?utm_source=nitrous.io&utm_medium=goop_readme&utm_campaign=goop_readme) Copyright (c) 2014 Irrational Industries, Inc. d.b.a. Nitrous.IO.
This software is licensed under the [MIT License](http://github.com/nitrous-io/goop/raw/master/LICENSE). ================================================ FILE: colors/colors.go ================================================ package colors const ( Reset = "\033[0m" OK = "\033[0;32m" Error = "\033[0;31m" Warn = "\033[0;33m" ) ================================================ FILE: goop/goget.go ================================================ package goop import ( "io" "os/exec" "regexp" ) var goGetDownloadRe = regexp.MustCompile(`(?m)^(\S+)\s+\(download\)$`) type DownloadRecorder struct { downloads map[string]struct{} writer io.Writer } func NewDownloadRecorder(writer io.Writer) *DownloadRecorder { return &DownloadRecorder{downloads: map[string]struct{}{}, writer: writer} } func (d *DownloadRecorder) Write(p []byte) (n int, err error) { s := string(p) matches := goGetDownloadRe.FindAllStringSubmatch(s, -1) if matches != nil { for _, m := range matches { d.downloads[m[1]] = struct{}{} } } return d.writer.Write(p) } func (d *DownloadRecorder) Downloads() []string { s := make([]string, 0, len(d.downloads)) for k, _ := range d.downloads { s = append(s, k) } return s } func (g *Goop) goGet(pkgpath string, gopath string) ([]string, error) { cmd := exec.Command("go", "get", "-d", "-v", "./...") env := g.patchedEnv(true) env["GOPATH"] = gopath cmd.Dir = pkgpath cmd.Env = env.Strings() cmd.Stdin = g.stdin cmd.Stdout = g.stdout dlRec := NewDownloadRecorder(g.stderr) cmd.Stderr = dlRec err := cmd.Run() if err != nil { return nil, err } return dlRec.Downloads(), nil } ================================================ FILE: goop/goop.go ================================================ package goop import ( "fmt" "io" "os" "os/exec" "path" "sort" "strings" "code.google.com/p/go.tools/go/vcs" "github.com/nitrous-io/goop/colors" "github.com/nitrous-io/goop/parser" "github.com/nitrous-io/goop/pkg/env" ) type UnsupportedVCSError struct { VCS string } func (e *UnsupportedVCSError) Error() string { return fmt.Sprintf("%s is not supported.", e.VCS) } type Goop struct { dir string stdin io.Reader stdout io.Writer stderr io.Writer } func NewGoop(dir string, stdin io.Reader, stdout io.Writer, stderr io.Writer) *Goop { return &Goop{dir: dir, stdin: stdin, stdout: stdout, stderr: stderr} } func (g *Goop) patchedEnv(replaceGopath bool) env.Env { e := env.NewEnv() binPath := path.Join(g.vendorDir(), "bin") if replaceGopath { e["GOPATH"] = g.vendorDir() } else { e.Prepend("GOPATH", g.vendorDir()) } e["GOBIN"] = binPath e.Prepend("PATH", binPath) return e } func (g *Goop) PrintEnv() { gopath := os.Getenv("GOPATH") if gopath == "" { g.stdout.Write([]byte(fmt.Sprintf("GOPATH=%s\n", g.vendorDir()))) } else { g.stdout.Write([]byte(fmt.Sprintf("GOPATH=%s:%s\n", g.vendorDir(), gopath))) } g.stdout.Write([]byte(fmt.Sprintf("PATH=%s:%s\n", path.Join(g.vendorDir(), "bin"), os.Getenv("PATH")))) } func (g *Goop) Exec(name string, args ...string) error { vname := path.Join(g.vendorDir(), "bin", name) _, err := os.Stat(vname) if err == nil { name = vname } cmd := exec.Command(name, args...) cmd.Env = g.patchedEnv(false).Strings() cmd.Stdin = g.stdin cmd.Stdout = g.stdout cmd.Stderr = g.stderr return cmd.Run() } func (g *Goop) Install() error { writeLockFile := false f, err := os.Open(path.Join(g.dir, "Goopfile.lock")) if err == nil { g.stdout.Write([]byte(colors.OK + "Using Goopfile.lock..." + colors.Reset + "\n")) } else { f, err = os.Open(path.Join(g.dir, "Goopfile")) if err != nil { return err } writeLockFile = true } return g.parseAndInstall(f, writeLockFile) } func (g *Goop) Update() error { f, err := os.Open(path.Join(g.dir, "Goopfile")) if err != nil { return err } return g.parseAndInstall(f, true) } func (g *Goop) parseAndInstall(goopfile *os.File, writeLockFile bool) error { defer goopfile.Close() deps, err := parser.Parse(goopfile) if err != nil { return err } srcPath := path.Join(g.vendorDir(), "src") tmpGoPath := path.Join(g.vendorDir(), "tmp") tmpSrcPath := path.Join(tmpGoPath, "src") err = os.RemoveAll(tmpGoPath) if err != nil { return err } err = os.MkdirAll(tmpSrcPath, 0775) if err != nil { return err } repos := map[string]*vcs.RepoRoot{} lockedDeps := map[string]*parser.Dependency{} for _, dep := range deps { if dep.URL == "" { g.stdout.Write([]byte(colors.OK + "=> Fetching " + dep.Pkg + "..." + colors.Reset + "\n")) } else { g.stdout.Write([]byte(colors.OK + "=> Fetching " + dep.Pkg + " from " + dep.URL + "..." + colors.Reset + "\n")) } repo, err := repoForDep(dep) if err != nil { return err } repos[dep.Pkg] = repo pkgPath := path.Join(srcPath, repo.Root) tmpPkgPath := path.Join(tmpSrcPath, repo.Root) err = os.MkdirAll(path.Join(tmpPkgPath, ".."), 0775) if err != nil { return err } noclone := false exists, err := pathExists(pkgPath) if err != nil { return err } tmpExists, err := pathExists(tmpPkgPath) if err != nil { return err } if exists { // if package already exists, just symlink package dir and skip cloning g.stderr.Write([]byte(colors.Warn + "Warning: " + pkgPath + " already exists; skipping!" + colors.Reset + "\n")) if !tmpExists { err = os.Symlink(pkgPath, tmpPkgPath) if err != nil { return err } } noclone = true } else { noclone = tmpExists } if !noclone { // clone repo err = g.clone(repo.VCS.Cmd, repo.Repo, tmpPkgPath) if err != nil { return err } } // if rev is not given, record current rev in path if dep.Rev == "" { rev, err := g.currentRev(repo.VCS.Cmd, tmpPkgPath) if err != nil { return err } dep.Rev = rev } lockedDeps[dep.Pkg] = dep // checkout specified rev err = g.checkout(repo.VCS.Cmd, tmpPkgPath, dep.Rev) if err != nil { return err } } for _, dep := range deps { g.stdout.Write([]byte(colors.OK + "=> Fetching dependencies for " + dep.Pkg + "..." + colors.Reset + "\n")) repo := repos[dep.Pkg] tmpPkgPath := path.Join(tmpSrcPath, repo.Root) // fetch sub-dependencies subdeps, err := g.goGet(tmpPkgPath, tmpGoPath) if err != nil { return err } for _, subdep := range subdeps { subdepRepo, err := vcs.RepoRootForImportPath(subdep, true) if err != nil { return err } subdepPkgPath := path.Join(tmpSrcPath, subdepRepo.Root) rev, err := g.currentRev(subdepRepo.VCS.Cmd, subdepPkgPath) if err != nil { return err } err = g.checkout(subdepRepo.VCS.Cmd, subdepPkgPath, rev) if err != nil { return err } repos[subdep] = subdepRepo lockedDeps[subdep] = &parser.Dependency{Pkg: subdep, Rev: rev} } } for _, dep := range lockedDeps { g.stdout.Write([]byte(colors.OK + "=> Installing " + dep.Pkg + "..." + colors.Reset + "\n")) repo := repos[dep.Pkg] pkgPath := path.Join(srcPath, repo.Root) tmpPkgPath := path.Join(tmpSrcPath, repo.Root) err = os.MkdirAll(path.Join(pkgPath, ".."), 0775) if err != nil { return err } lfi, err := os.Lstat(tmpPkgPath) if err != nil && !os.IsNotExist(err) { return err } if err == nil { if lfi.Mode()&os.ModeSymlink == 0 { // move package to vendor path err = os.RemoveAll(pkgPath) if err != nil { return err } err = os.Rename(tmpPkgPath, pkgPath) } else { // package already in vendor path, just remove the symlink err = os.Remove(tmpPkgPath) } if err != nil { return err } } } for _, dep := range lockedDeps { // install repo := repos[dep.Pkg] pkgPath := path.Join(srcPath, repo.Root) cmd := g.command(pkgPath, "go", "install", "-x", dep.Pkg) cmd.Env = g.patchedEnv(true).Strings() cmd.Run() } err = os.RemoveAll(tmpGoPath) if err != nil { return err } // in order to minimize diffs, we sort lockedDeps first and write the // sorted results if writeLockFile { lf, err := os.Create(path.Join(g.dir, "Goopfile.lock")) defer lf.Close() var keys []string for k := range lockedDeps { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { dep := lockedDeps[k] _, err = lf.WriteString(dep.String() + "\n") if err != nil { return err } } } g.stdout.Write([]byte(colors.OK + "=> Done!" + colors.Reset + "\n")) return nil } func (g *Goop) vendorDir() string { return path.Join(g.dir, ".vendor") } func (g *Goop) currentRev(vcsCmd string, path string) (string, error) { switch vcsCmd { case "git": cmd := exec.Command("git", "rev-parse", "--verify", "HEAD") cmd.Dir = path cmd.Stderr = g.stderr rev, err := cmd.Output() if err != nil { return "", err } else { return strings.TrimSpace(string(rev)), err } case "hg": cmd := exec.Command("hg", "log", "-r", ".", "--template", "{node}") cmd.Dir = path cmd.Stderr = g.stderr rev, err := cmd.Output() if err != nil { return "", err } else { return strings.TrimSpace(string(rev)), err } } return "", &UnsupportedVCSError{VCS: vcsCmd} } func (g *Goop) clone(vcsCmd string, url string, clonePath string) error { switch vcsCmd { case "git": return g.command("", "git", "clone", url, clonePath).Run() case "hg": return g.command("", "hg", "clone", url, clonePath).Run() } return &UnsupportedVCSError{VCS: vcsCmd} } func (g *Goop) checkout(vcsCmd string, path string, tag string) error { g.stdout.Write([]byte("Checking out \"" + tag + "\"\n")) switch vcsCmd { case "git": err := g.command(path, "git", "fetch").Run() if err != nil { return err } return g.quietCommand(path, "git", "checkout", tag).Run() case "hg": err := g.command(path, "hg", "pull").Run() if err != nil { return err } return g.quietCommand(path, "hg", "update", tag).Run() } return &UnsupportedVCSError{VCS: vcsCmd} } func (g *Goop) command(path string, name string, args ...string) *exec.Cmd { cmd := exec.Command(name, args...) cmd.Dir = path cmd.Stdin = g.stdin cmd.Stdout = g.stdout cmd.Stderr = g.stderr return cmd } func (g *Goop) quietCommand(path string, name string, args ...string) *exec.Cmd { cmd := g.command(path, name, args...) cmd.Stdout = nil cmd.Stderr = nil return cmd } func repoForDep(dep *parser.Dependency) (*vcs.RepoRoot, error) { if dep.URL != "" { return RepoRootForImportPathWithURLOverride(dep.Pkg, dep.URL) } return vcs.RepoRootForImportPath(dep.Pkg, true) } // pathExists returns: // * (true, nil) if path exists // * (false, nil) if path does not exist // * (false, err) if error happened during stat func pathExists(path string) (bool, error) { _, err := os.Stat(path) switch { case err != nil && !os.IsNotExist(err): // unexpected err return false, err case err != nil && os.IsNotExist(err): return false, nil case err == nil: return true, nil default: panic("never reached") } } ================================================ FILE: goop/goop_test.go ================================================ package goop_test import ( "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) func Test(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "goop") } ================================================ FILE: goop/vcs.go ================================================ package goop import ( "os/exec" "strings" "code.google.com/p/go.tools/go/vcs" ) func GuessVCS(url string) string { switch { case strings.HasPrefix(url, "https://github.com"): return "git" case strings.HasPrefix(url, "git://"): return "git" case strings.HasPrefix(url, "git+ssh://"): return "git" case strings.HasPrefix(url, "git@"): return "git" case strings.HasPrefix(url, "ssh://hg@"): return "hg" default: return "" } } func IdentifyVCS(url string) string { v := map[string][]string{ "git": []string{"git", "ls-remote"}, "hg": []string{"hg", "identify"}, } tryVCS := func(vcs string) bool { cmd := v[vcs] delete(v, vcs) return exec.Command(cmd[0], append(cmd[1:], url)...).Run() == nil // use vcs.VCS.Ping? } guess := GuessVCS(url) if guess != "" && v[guess] != nil { if tryVCS(guess) { return guess } } for k, _ := range v { if tryVCS(k) { return k } } return "" } func RepoRootForImportPathWithURLOverride(importPath string, url string) (*vcs.RepoRoot, error) { repo, err := vcs.RepoRootForImportPathStatic(importPath, "ignore") if err != nil { return nil, err } repo.Repo = url return repo, nil } ================================================ FILE: goop/vcs_test.go ================================================ package goop_test import ( "github.com/nitrous-io/goop/goop" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("vcs", func() { tests := []struct { importPath string url string guess string actual string repoRoot string }{ {"github.com/dotcloud/docker/pkg/term", "git://github.com/dotcloud/docker.git", "git", "git", "github.com/dotcloud/docker"}, {"github.com/mattn/go-sqlite3", "git://github.com/mattn/go-sqlite3.git", "git", "git", "github.com/mattn/go-sqlite3"}, {"github.com/mattn/go-sqlite3", "https://github.com/mattn/go-sqlite3.git", "git", "git", "github.com/mattn/go-sqlite3"}, {"github.com/mattn/go-sqlite3", "git+ssh://git@github.com/mattn/go-sqlite3.git", "git", "git", "github.com/mattn/go-sqlite3"}, {"github.com/mattn/go-sqlite3", "git@github.com:mattn/go-sqlite3.git", "git", "git", "github.com/mattn/go-sqlite3"}, {"github.com/nitrous-io/no-such", "git@github.com/nitrous-io/no-such", "git", "", "github.com/nitrous-io/no-such"}, {"bitbucket.org/kardianos/osext", "ssh://hg@bitbucket.org/kardianos/osext", "hg", "hg", "bitbucket.org/kardianos/osext"}, {"bitbucket.org/kardianos/osext", "https://bitbucket.org/kardianos/osext", "", "hg", "bitbucket.org/kardianos/osext"}, {"bitbucket.org/ymotongpoo/go-bitarray", "git@bitbucket.org:ymotongpoo/go-bitarray.git", "git", "git", "bitbucket.org/ymotongpoo/go-bitarray"}, {"bitbucket.org/ymotongpoo/go-bitarray", "https://bitbucket.org/ymotongpoo/go-bitarray.git", "", "git", "bitbucket.org/ymotongpoo/go-bitarray"}, {"code.google.com/p/go.tools/go/vcs", "https://code.google.com/p/go.tools/", "", "hg", "code.google.com/p/go.tools"}, // not supported yet - {"example.com/foo/go-sqlite3", "git@github.com:mattn/go-sqlite3.git", "git", "git", "example.com/foo/go-sqlite3"}, } Describe("GuessVCS()", func() { for _, test := range tests { t := test Context(t.url, func() { It("returns "+t.guess, func() { Expect(goop.GuessVCS(t.url)).To(Equal(t.guess)) }) }) } }) // slow test: this test requires network connection XDescribe("IdentifyVCS()", func() { for _, test := range tests { t := test Context(t.url, func() { It("returns "+t.actual, func() { Expect(goop.IdentifyVCS(t.url)).To(Equal(t.actual)) }) }) } }) Describe("RepoRootForImportPathWithURLOverride()", func() { for _, test := range tests { t := test Context(t.url, func() { It("returns "+t.repoRoot, func() { repo, err := goop.RepoRootForImportPathWithURLOverride(t.importPath, t.url) Expect(err).To(BeNil()) Expect(repo).NotTo(BeNil()) Expect(repo.Repo).To(Equal(t.url)) Expect(repo.Root).To(Equal(t.repoRoot)) }) }) } }) }) ================================================ FILE: main.go ================================================ package main import ( "errors" "os" "path" "strconv" "strings" "github.com/nitrous-io/goop/colors" "github.com/nitrous-io/goop/goop" ) func main() { name := path.Base(os.Args[0]) pwd, err := os.Getwd() if err != nil { os.Stderr.WriteString(colors.Error + name + ": failed to determine present working directory!" + colors.Reset + "\n") } g := goop.NewGoop(path.Join(pwd), os.Stdin, os.Stdout, os.Stderr) if len(os.Args) < 2 { printUsage() } cmd := os.Args[1] switch cmd { case "help": printUsage() case "install": err = g.Install() case "update": err = g.Update() case "exec": if len(os.Args) < 3 { printUsage() } err = g.Exec(os.Args[2], os.Args[3:]...) case "go": if len(os.Args) < 3 { printUsage() } err = g.Exec("go", os.Args[2:]...) case "env": g.PrintEnv() default: err = errors.New(`unrecognized command "` + cmd + `"`) } if err != nil { errMsg := err.Error() code := 1 // go does not provide a cross-platform way to get exit status, so inspect error message instead // https://code.google.com/p/go/source/browse/src/pkg/os/exec_posix.go#119 if strings.HasPrefix(errMsg, "exit status ") { code, err = strconv.Atoi(errMsg[len("exit status "):]) if err != nil { code = 1 } errMsg = "Command failed with " + errMsg } os.Stderr.WriteString(colors.Error + name + ": " + errMsg + colors.Reset + "\n") os.Exit(code) } } func printUsage() { os.Stdout.WriteString(strings.TrimSpace(usage) + "\n\n") os.Exit(0) } const usage = ` Goop is a tool for managing Go dependencies. goop command [arguments] The commands are: install install the dependencies specified by Goopfile or Goopfile.lock update update dependencies to their latest versions env print GOPATH and PATH environment variables, with the vendor path prepended exec execute a command in the context of the installed dependencies go execute a go command in the context of the installed dependencies help print this message ` ================================================ FILE: parser/dependency.go ================================================ package parser import "strings" type Dependency struct { Pkg string Rev string URL string } func (d *Dependency) String() string { s := make([]string, 0, 3) s = append(s, d.Pkg) if d.Rev != "" { s = append(s, "#"+d.Rev) } if d.URL != "" { s = append(s, "!"+d.URL) } return strings.Join(s, " ") } ================================================ FILE: parser/parser.go ================================================ package parser import ( "bufio" "fmt" "io" "strings" ) type ParseError struct { LineNum uint LineText string Message string } const ( CommentOrPackage = iota URLOr ) const ( TokenComment = "//" TokenRev = "#" TokenURL = "!" ) func (e *ParseError) Error() string { return fmt.Sprintf("Parse failed at line %d - %s\n %s", e.LineNum, e.LineText, e.Message) } func Parse(r io.Reader) ([]*Dependency, error) { s := bufio.NewScanner(r) ln := uint(0) deps := []*Dependency{} for s.Scan() { ln++ line := strings.TrimSpace(s.Text()) tokens := strings.Fields(line) if line == "" || strings.HasPrefix(tokens[0], TokenComment) { continue } dep := &Dependency{Pkg: tokens[0]} parseErr := &ParseError{LineNum: ln, LineText: line} for _, t := range tokens[1:] { if strings.HasPrefix(t, TokenComment) { break } switch { case strings.HasPrefix(t, TokenRev): if dep.Rev != "" { parseErr.Message = "Multiple revisions given" return nil, parseErr } dep.Rev = t[1:] case strings.HasPrefix(t, TokenURL): if dep.URL != "" { parseErr.Message = "Multiple URLs given" return nil, parseErr } dep.URL = t[1:] default: parseErr.Message = "Unrecognized token given" return nil, parseErr } } deps = append(deps, dep) } if err := s.Err(); err != nil { return nil, err } return deps, nil } ================================================ FILE: parser/parser_test.go ================================================ package parser_test import ( "bytes" "testing" "github.com/nitrous-io/goop/parser" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) func Test(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "parser") } var _ = Describe("parser", func() { Describe("Parse()", func() { var ( deps []*parser.Dependency err error ) Context("empty Goopfile", func() { BeforeEach(func() { deps, err = parser.Parse(bytes.NewBufferString("")) }) It("returns an empty slice", func() { Expect(err).To(BeNil()) Expect(deps).NotTo(BeNil()) Expect(deps).To(HaveLen(0)) }) }) Context("one entry", func() { Context("with no revision specified", func() { BeforeEach(func() { deps, err = parser.Parse(bytes.NewBufferString(` github.com/nitrous-io/goop `)) }) It("parses and returns a slice containing one dependency item", func() { Expect(err).To(BeNil()) Expect(deps).To(HaveLen(1)) Expect(deps[0]).To(Equal(&parser.Dependency{Pkg: "github.com/nitrous-io/goop", Rev: ""})) }) }) Context("with revision specified", func() { It("parses and returns a slice containing one dependency item", func() { deps, err = parser.Parse(bytes.NewBufferString(` github.com/nitrous-io/goop #09f0feb1b103933bd9985f0a85e01eeaad8d75c8 `)) Expect(err).To(BeNil()) Expect(deps).To(HaveLen(1)) Expect(deps[0]).To(Equal(&parser.Dependency{ Pkg: "github.com/nitrous-io/goop", Rev: "09f0feb1b103933bd9985f0a85e01eeaad8d75c8", })) }) It("ignores whitespace", func() { deps, err = parser.Parse(bytes.NewBufferString(` github.com/nitrous-io/goop #09f0feb1b103933bd9985f0a85e01eeaad8d75c8 `)) Expect(err).To(BeNil()) Expect(deps).To(HaveLen(1)) Expect(deps[0]).To(Equal(&parser.Dependency{ Pkg: "github.com/nitrous-io/goop", Rev: "09f0feb1b103933bd9985f0a85e01eeaad8d75c8", })) }) }) Context("with custom repo url", func() { BeforeEach(func() { deps, err = parser.Parse(bytes.NewBufferString(` github.com/nitrous-io/goop !git@github.com:foo/goop `)) }) It("parses and returns a slice containing one dependency item", func() { Expect(err).To(BeNil()) Expect(deps).To(HaveLen(1)) Expect(deps[0]).To(Equal(&parser.Dependency{ Pkg: "github.com/nitrous-io/goop", URL: "git@github.com:foo/goop", })) }) }) Context("with a comment", func() { BeforeEach(func() { deps, err = parser.Parse(bytes.NewBufferString(` github.com/nitrous-io/goop // hello world `)) }) }) Context("with unparseable garbage", func() { BeforeEach(func() { deps, err = parser.Parse(bytes.NewBufferString(` github.com/nitrous-io/goop (*@#&!@(*#)@$F@sdgu8$! `)) }) It("fails and returns parse error", func() { Expect(err).NotTo(BeNil()) Expect(deps).To(BeNil()) }) }) }) Context("multiple entries", func() { BeforeEach(func() { deps, err = parser.Parse(bytes.NewBufferString(` github.com/nitrous-io/goop #09f0feb1b103933bd9985f0a85e01eeaad8d75c8 github.com/gorilla/mux github.com/gorilla/context #14f550f51af52180c2eefed15e5fd18d63c0a64a // future versions don't work github.com/foo/bar #ffffffffffffffffffffffffffffffffffffffff !git@github.com:baz/bar // don't upgrade this to 1.0.4 github.com/hello/world !git@github.com:bye/world #v1.0.3 // I REPEAT, DON'T! `)) }) It("parses and returns a slice containing multiple dependency items", func() { Expect(err).To(BeNil()) Expect(deps).To(HaveLen(5)) Expect(deps[0]).To(Equal(&parser.Dependency{ Pkg: "github.com/nitrous-io/goop", Rev: "09f0feb1b103933bd9985f0a85e01eeaad8d75c8", })) Expect(deps[1]).To(Equal(&parser.Dependency{ Pkg: "github.com/gorilla/mux", Rev: "", })) Expect(deps[2]).To(Equal(&parser.Dependency{ Pkg: "github.com/gorilla/context", Rev: "14f550f51af52180c2eefed15e5fd18d63c0a64a", })) Expect(deps[3]).To(Equal(&parser.Dependency{ Pkg: "github.com/foo/bar", Rev: "ffffffffffffffffffffffffffffffffffffffff", URL: "git@github.com:baz/bar", })) Expect(deps[4]).To(Equal(&parser.Dependency{ Pkg: "github.com/hello/world", Rev: "v1.0.3", URL: "git@github.com:bye/world", })) }) }) }) }) ================================================ FILE: pkg/env/env.go ================================================ package env import ( "bytes" "os" ) type Env map[string]string func NewEnv() Env { e := Env{} osenv := os.Environ() for _, l := range osenv { kv := bytes.SplitN([]byte(l), []byte("="), 2) k := string(kv[0]) if len(kv) == 2 { e[k] = string(kv[1]) } else { e[k] = "" } } return e } func (e Env) Strings() []string { s := make([]string, 0, len(e)) for k, v := range e { s = append(s, k+"="+v) } return s } func (e Env) Prepend(key string, val string) { oldv := e[key] if oldv == "" { e[key] = val return } e[key] = val + ":" + oldv } ================================================ FILE: pkg/env/env_test.go ================================================ package env_test import ( "os" "testing" "github.com/nitrous-io/goop/pkg/env" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) func Test(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "env") } var _ = Describe("env", func() { var e env.Env BeforeEach(func() { os.Setenv("_GOOP_ENV_TEST_FOO", "foo") os.Setenv("_GOOP_ENV_TEST_BAR", "bar=bar bar") os.Setenv("_GOOP_ENV_TEST_EMPTY", "") e = env.NewEnv() }) AfterEach(func() { os.Setenv("_GOOP_ENV_TEST_FOO", "") os.Setenv("_GOOP_ENV_TEST_BAR", "") os.Setenv("_GOOP_ENV_TEST_EMPTY", "") }) Describe("NewEnv()", func() { It("returns a new env map using current env vars", func() { Expect(e["_GOOP_ENV_TEST_FOO"]).To(Equal("foo")) Expect(e["_GOOP_ENV_TEST_BAR"]).To(Equal("bar=bar bar")) Expect(e["_GOOP_ENV_TEST_EMPTY"]).To(BeEmpty()) }) }) Describe("Strings()", func() { It("returns a copy of strings representing the env, in the form key=value", func() { s := e.Strings() Expect(s).To(ContainElement("_GOOP_ENV_TEST_FOO=foo")) Expect(s).To(ContainElement("_GOOP_ENV_TEST_BAR=bar=bar bar")) Expect(s).To(ContainElement("_GOOP_ENV_TEST_EMPTY=")) }) }) Describe("Prepend()", func() { Context("when a given key has a value", func() { It("prepends new value to the existing value", func() { e.Prepend("_GOOP_ENV_TEST_FOO", "lol") Expect(e["_GOOP_ENV_TEST_FOO"]).To(Equal("lol:foo")) e.Prepend("_GOOP_ENV_TEST_FOO", "hello") Expect(e["_GOOP_ENV_TEST_FOO"]).To(Equal("hello:lol:foo")) }) }) Context("when a given key is empty", func() { It("sets new value", func() { e.Prepend("_GOOP_ENV_TEST_EMPTY", "foo") Expect(e["_GOOP_ENV_TEST_EMPTY"]).To(Equal("foo")) e.Prepend("_GOOP_ENV_TEST_EMPTY", "lol") Expect(e["_GOOP_ENV_TEST_EMPTY"]).To(Equal("lol:foo")) }) }) Context("when a given key does not exist", func() { BeforeEach(func() { delete(e, "_GOOP_ENV_TEST_EMPTY") }) It("sets new value", func() { e.Prepend("_GOOP_ENV_TEST_EMPTY", "foo") Expect(e["_GOOP_ENV_TEST_EMPTY"]).To(Equal("foo")) e.Prepend("_GOOP_ENV_TEST_EMPTY", "lol") Expect(e["_GOOP_ENV_TEST_EMPTY"]).To(Equal("lol:foo")) }) }) }) })