Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package main

import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"

"github.com/jingweno/nut/vendor/_nuts/github.com/codegangsta/cli"
)

// dependencyFinder is an interface for finding project dependencies.
// This can later be used to migrate from godep, etc
type dependencyFinder interface {
FindDependencies(vcsRefs bool) (map[string]string, error)
}

var initCmd = cli.Command{
Name: "init",
Usage: "init nut on an existing go project, writing all dependencies to Nut.toml",
Action: runInit,
Flags: []cli.Flag{
cli.StringSliceFlag{
Name: "ignore, i",
Usage: "import path prefixes to ignore",
Value: &cli.StringSlice{},
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "verbose output",
},
cli.BoolFlag{
Name: "refs, r",
Usage: "extract VCS refs for packages",
},
},
}

// depFinder finds the dependencies of an existing project by traversing go deps
type depFinder struct {
// the seen dependencies during traversal
deps map[string]*pkg

// the root package. we ignore all its sub packages
root string

// a list of ignore import prefixes (e.g. gitlab.mydomain.com)
ignored []string

// the current working directory (this changes durgin traversal)
wd string

// output verbose messages during traversal
verbose bool

lister pkgLister
}

// isIgnored checks if an import path start with a prefix that has been set to be ignored
func (c *depFinder) isIgnored(path string) bool {

for _, prefix := range c.ignored {
if strings.HasPrefix(path, prefix) {
return true
}
}

return false
}

// findDeps recursively finds all the dependencies of a package that are not part of the same
// project or the standard library
func (c *depFinder) findDeps(p *pkg) error {

if c.root == "" {
c.root = p.ImportPath
}

deps, err := c.lister.List(p.Deps...)
if err != nil {
return err
}

for _, dep := range deps {
if dep.Standard {
continue
}

// do not scan deps twice
if _, found := c.deps[dep.ImportPath]; found {
continue
}

if !strings.HasPrefix(dep.ImportPath, c.root) && !c.isIgnored(dep.ImportPath) {
if c.verbose {
fmt.Println(dep.ImportPath)
}
c.deps[dep.ImportPath] = dep

} else {
// we put ignored packages in the dep map as nils just so we'll scan them once for their deps
c.deps[dep.ImportPath] = nil
}

// we find deps even on "ignored" packages - we want their deps
c.findDeps(dep)
}

return nil
}

// findDepsInDir finds all the deps for the packages under the dir in path.
// this is used to recursively traverse all packages in a project
func (c *depFinder) findDepsInDir(pth string) error {

c.wd = pth
os.Chdir(c.wd)

pkgs, err := c.lister.List("")
if err != nil {
return err
}
for _, p := range pkgs {
c.findDeps(p)
}

files, err := ioutil.ReadDir(pth)

if err != nil {
return err
}

for _, file := range files {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could call go list -e -json ./... to return all deps in current path instead of traversing folders. It requires some code changes in https://github.com/jingweno/nut/blob/master/pkg.go#L246 though.

if !file.IsDir() {
continue
}

if strings.HasPrefix(file.Name(), ".") || strings.HasPrefix(file.Name(), "_") {
continue
}

err := c.findDepsInDir(path.Join(pth, file.Name()))
if err != nil {
fmt.Println("error traversing", file.Name(), ": ", err)
return err
}

}

return nil

}

// FindDependencies traverses the current WD and retrieves a dependency map (package => revision)
// for all 3rd party libraries.
// VCS refs are retrieved only if the vcsRefs flag is set to true
func (c *depFinder) FindDependencies(vcsRefs bool) (map[string]string, error) {
err := c.findDepsInDir(c.wd)
if err != nil {
return nil, err
}

ret := make(map[string]string)
if c.verbose && vcsRefs {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused on vcsRefs. Are we trying to get the SHA from dependencies in the GOPATH?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, where else would we get them from? This is a "virgin" repo that we assume had no vendoring solution before, so we can just "freeze" the current state of GOPATH's revisions.

fmt.Println("Finding VCS refs for packages")
}

for dep, pkg := range c.deps {

// nil pkg's are placeholders for not entering the same dir twice, we ignore them
if pkg == nil {
continue
}
tag := ""
if vcsRefs {
_, vc, err := VCSForImportPath(dep)

if err == nil {
tag, _ = vc.Identify(pkg.Dir)
} else {
fmt.Printf("Error extracting tag for %s: %s\n", dep, err)
}

}
ret[dep] = tag

}

return ret, nil
}

func runInit(c *cli.Context) {

//the first arg can be the path of a project
if len(c.Args()) > 0 {
pth := c.Args().First()
if pth != "" && pth != "." {
err := os.Chdir(pth)
if err != nil {
fmt.Printf("Invalid path %s (%s)\n", pth, err)
os.Exit(1)
}
}
}

cwd, err := os.Getwd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}

ctx := &depFinder{
deps: make(map[string]*pkg),
ignored: c.StringSlice("ignored"),
wd: cwd,
verbose: c.Bool("verbose"),
lister: pkgLister{
Env: os.Environ(),
},
}

deps, err := ctx.FindDependencies(c.Bool("refs"))

// create a manifest from the gathered data
manifest := Manifest{
App: ManifestApp{
Name: path.Base(cwd),
Authors: []string{"Your Name <you@example.com>"},
Version: "0.0.1",
},
Deps: ManifestDeps(deps),
}

os.Chdir(cwd)

err = manifest.write()
if err != nil {
fmt.Println(err)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, it would be nice to print error to STDERR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. I'll change that. it's weird BTW that the cli package doesn't allow you to return an error from running the app. any idea why that is?

os.Exit(1)
}

fmt.Println("New manifest file written to", setting.ConfigFile)

}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func main() {
app.Commands = []cli.Command{
installCmd,
newCmd,
initCmd,
}

app.Run(os.Args)
Expand Down
23 changes: 22 additions & 1 deletion manifest.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package main

import "github.com/jingweno/nut/vendor/_nuts/github.com/BurntSushi/toml"
import (
"fmt"
"os"

"github.com/jingweno/nut/vendor/_nuts/github.com/BurntSushi/toml"
)

type Manifest struct {
App ManifestApp `toml:"application"`
Expand All @@ -24,3 +29,19 @@ func loadManifest() (*Manifest, error) {

return &m, nil
}

// write writes the manifest data to disk
func (m Manifest) write() error {
fp, err := os.Create(setting.ConfigFile)
if err != nil {
return fmt.Errorf("Error writing manifest: %s", err)
}
defer fp.Close()

err = toml.NewEncoder(fp).Encode(m)
if err != nil {
return fmt.Errorf("Error writing manifest: %s", err)
}

return nil
}