diff --git a/context.go b/context.go index 1db140b4c9..0603f34139 100644 --- a/context.go +++ b/context.go @@ -17,6 +17,8 @@ type ctx struct { GOPATH string // Go path } +// newContext creates a struct with the project's GOPATH. It assumes +// that of your "GOPATH"'s we want the one we are currently in. func newContext() (*ctx, error) { // this way we get the default GOPATH that was added in 1.8 buildContext := build.Default diff --git a/init.go b/init.go index 9ce26394b6..f449bb1b33 100644 --- a/init.go +++ b/init.go @@ -97,13 +97,173 @@ func runInit(args []string) error { } defer sm.Release() - // TODO: This is just wrong, need to figure out manifest file structure + pd, err := getProjectData(pkgT, cpr, sm) + if err != nil { + return err + } m := manifest{ - Dependencies: make(gps.ProjectConstraints), + Dependencies: pd.constraints, + } + + // Make an initial lock from what knowledge we've collected about the + // versions on disk + l := lock{ + P: make([]gps.LockedProject, 0, len(pd.ondisk)), + } + + for pr, v := range pd.ondisk { + // That we have to chop off these path prefixes is a symptom of + // a problem in gps itself + pkgs := make([]string, 0, len(pd.dependencies[pr])) + prslash := string(pr) + "/" + for _, pkg := range pd.dependencies[pr] { + if pkg == string(pr) { + pkgs = append(pkgs, ".") + } else { + pkgs = append(pkgs, strings.TrimPrefix(pkg, prslash)) + } + } + + l.P = append(l.P, gps.NewLockedProject( + gps.ProjectIdentifier{ProjectRoot: pr}, v, pkgs), + ) + } + + var l2 *lock + if len(pd.notondisk) > 0 { + vlogf("Solving...") + params := gps.SolveParameters{ + RootDir: root, + RootPackageTree: pkgT, + Manifest: &m, + Lock: &l, + } + + if *verbose { + params.Trace = true + params.TraceLogger = log.New(os.Stderr, "", 0) + } + s, err := gps.Prepare(params, sm) + if err != nil { + return errors.Wrap(err, "prepare solver") + } + + soln, err := s.Solve() + if err != nil { + handleAllTheFailuresOfTheWorld(err) + return err + } + l2 = lockFromInterface(soln) + } else { + l2 = &l + } + + vlogf("Writing manifest and lock files.") + if err := writeFile(mf, &m); err != nil { + return errors.Wrap(err, "writeFile for manifest") + } + if err := writeFile(lf, l2); err != nil { + return errors.Wrap(err, "writeFile for lock") + } + + return nil +} + +// contains checks if a array of strings contains a value +func contains(a []string, b string) bool { + for _, v := range a { + if b == v { + return true + } + } + return false +} + +// isStdLib reports whether $GOROOT/src/path should be considered +// part of the standard distribution. For historical reasons we allow people to add +// their own code to $GOROOT instead of using $GOPATH, but we assume that +// code will start with a domain name (dot in the first element). +// This was loving taken from src/cmd/go/pkg.go in Go's code (isStandardImportPath). +func isStdLib(path string) bool { + i := strings.Index(path, "/") + if i < 0 { + i = len(path) + } + elem := path[:i] + return !strings.Contains(elem, ".") +} + +// TODO solve failures can be really creative - we need to be similarly creative +// in handling them and informing the user appropriately +func handleAllTheFailuresOfTheWorld(err error) { + fmt.Printf("ouchie, solve error: %s", err) +} + +func writeFile(path string, in json.Marshaler) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + b, err := in.MarshalJSON() + if err != nil { + return err + } + + _, err = f.Write(b) + return err +} + +func isRegular(name string) (bool, error) { + // TODO: lstat? + fi, err := os.Stat(name) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, err + } + if fi.IsDir() { + return false, fmt.Errorf("%q is a directory, should be a file", name) + } + return true, nil +} + +func isDir(name string) (bool, error) { + // TODO: lstat? + fi, err := os.Stat(name) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, err + } + if !fi.IsDir() { + return false, fmt.Errorf("%q is not a directory", name) } + return true, nil +} +func hasImportPathPrefix(s, prefix string) bool { + if s == prefix { + return true + } + return strings.HasPrefix(s, prefix+"/") +} + +type projectData struct { + constraints gps.ProjectConstraints // constraints that could be found + dependencies map[gps.ProjectRoot][]string // all dependencies (imports) found by project root + notondisk map[gps.ProjectRoot]bool // projects that were not found on disk + ondisk map[gps.ProjectRoot]gps.Version // projects that were found on disk +} + +func getProjectData(pkgT gps.PackageTree, cpr string, sm *gps.SourceMgr) (projectData, error) { vlogf("Building dependency graph...") - processed := make(map[gps.ProjectRoot][]string) + + constraints := make(gps.ProjectConstraints) + dependencies := make(map[gps.ProjectRoot][]string) packages := make(map[string]bool) notondisk := make(map[gps.ProjectRoot]bool) ondisk := make(map[gps.ProjectRoot]gps.Version) @@ -125,24 +285,24 @@ func runInit(args []string) error { } pr, err := sm.DeduceProjectRoot(ip) if err != nil { - return errors.Wrap(err, "sm.DeduceProjectRoot") // TODO: Skip and report ? + return projectData{}, errors.Wrap(err, "sm.DeduceProjectRoot") // TODO: Skip and report ? } packages[ip] = true - if _, ok := processed[pr]; ok { - if !contains(processed[pr], ip) { - processed[pr] = append(processed[pr], ip) + if _, ok := dependencies[pr]; ok { + if !contains(dependencies[pr], ip) { + dependencies[pr] = append(dependencies[pr], ip) } continue } vlogf("Package %q has import %q, analyzing...", v.P.ImportPath, ip) - processed[pr] = []string{ip} + dependencies[pr] = []string{ip} v, err := depContext.versionInWorkspace(pr) if err != nil { notondisk[pr] = true - logf("Could not determine version for %q, omitting from generated manifest", pr) + vlogf("Could not determine version for %q, omitting from generated manifest", pr) continue } @@ -156,7 +316,7 @@ func runInit(args []string) error { pp.Constraint = c } - m.Dependencies[pr] = pp + constraints[pr] = pp } } @@ -227,12 +387,12 @@ func runInit(args []string) error { return nil } - if _, ok := processed[pr]; ok { - if !contains(processed[pr], pkg) { - processed[pr] = append(processed[pr], pkg) + if _, ok := dependencies[pr]; ok { + if !contains(dependencies[pr], pkg) { + dependencies[pr] = append(dependencies[pr], pkg) } } else { - processed[pr] = []string{pkg} + dependencies[pr] = []string{pkg} } // project must be on disk at this point; question is @@ -272,153 +432,15 @@ func runInit(args []string) error { for pkg := range packages { err := dft(pkg) if err != nil { - return err // already errors.Wrap()'d internally - } - } - - // Make an initial lock from what knowledge we've collected about the - // versions on disk - l := lock{ - P: make([]gps.LockedProject, 0, len(ondisk)), - } - - for pr, v := range ondisk { - // That we have to chop off these path prefixes is a symptom of - // a problem in gps itself - pkgs := make([]string, 0, len(processed[pr])) - prslash := string(pr) + "/" - for _, pkg := range processed[pr] { - if pkg == string(pr) { - pkgs = append(pkgs, ".") - } else { - pkgs = append(pkgs, strings.TrimPrefix(pkg, prslash)) - } - } - - l.P = append(l.P, gps.NewLockedProject( - gps.ProjectIdentifier{ProjectRoot: pr}, v, pkgs), - ) - } - - var l2 *lock - if len(notondisk) > 0 { - vlogf("Solving...") - params := gps.SolveParameters{ - RootDir: root, - RootPackageTree: pkgT, - Manifest: &m, - Lock: &l, - } - - if *verbose { - params.Trace = true - params.TraceLogger = log.New(os.Stderr, "", 0) - } - s, err := gps.Prepare(params, sm) - if err != nil { - return errors.Wrap(err, "prepare solver") - } - - soln, err := s.Solve() - if err != nil { - handleAllTheFailuresOfTheWorld(err) - return err + return projectData{}, err // already errors.Wrap()'d internally } - l2 = lockFromInterface(soln) - } else { - l2 = &l - } - - vlogf("Writing manifest and lock files.") - if err := writeFile(mf, &m); err != nil { - return errors.Wrap(err, "writeFile for manifest") } - if err := writeFile(lf, l2); err != nil { - return errors.Wrap(err, "writeFile for lock") - } - - return nil -} -// contains checks if a array of strings contains a value -func contains(a []string, b string) bool { - for _, v := range a { - if b == v { - return true - } + pd := projectData{ + constraints: constraints, + dependencies: dependencies, + notondisk: notondisk, + ondisk: ondisk, } - return false -} - -// isStdLib reports whether $GOROOT/src/path should be considered -// part of the standard distribution. For historical reasons we allow people to add -// their own code to $GOROOT instead of using $GOPATH, but we assume that -// code will start with a domain name (dot in the first element). -// This was loving taken from src/cmd/go/pkg.go in Go's code (isStandardImportPath). -func isStdLib(path string) bool { - i := strings.Index(path, "/") - if i < 0 { - i = len(path) - } - elem := path[:i] - return !strings.Contains(elem, ".") -} - -// TODO solve failures can be really creative - we need to be similarly creative -// in handling them and informing the user appropriately -func handleAllTheFailuresOfTheWorld(err error) { - fmt.Printf("ouchie, solve error: %s", err) -} - -func writeFile(path string, in json.Marshaler) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer f.Close() - - b, err := in.MarshalJSON() - if err != nil { - return err - } - - _, err = f.Write(b) - return err -} - -func isRegular(name string) (bool, error) { - // TODO: lstat? - fi, err := os.Stat(name) - if os.IsNotExist(err) { - return false, nil - } - if err != nil { - return false, err - } - if fi.IsDir() { - return false, fmt.Errorf("%q is a directory, should be a file", name) - } - return true, nil -} - -func isDir(name string) (bool, error) { - // TODO: lstat? - fi, err := os.Stat(name) - if os.IsNotExist(err) { - return false, nil - } - if err != nil { - return false, err - } - if !fi.IsDir() { - return false, fmt.Errorf("%q is not a directory", name) - } - return true, nil -} - -func hasImportPathPrefix(s, prefix string) bool { - if s == prefix { - return true - } - return strings.HasPrefix(s, prefix+"/") + return pd, nil } diff --git a/main.go b/main.go index a3f007e7a2..252aea243d 100644 --- a/main.go +++ b/main.go @@ -95,6 +95,7 @@ var commands = []*command{ initCmd, statusCmd, ensureCmd, + removeCmd, // help added here at init time. } diff --git a/remove.go b/remove.go new file mode 100644 index 0000000000..3a01c723dd --- /dev/null +++ b/remove.go @@ -0,0 +1,120 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/sdboyer/gps" +) + +var removeCmd = &command{ + fn: runRemove, + name: "rm", + short: `[flags] [packages] + Remove a package or a set of packages. + `, + long: ` +Run it when: +To stop using dependencies +To clean out unused dependencies + +What it does +Removes the given dependency from the Manifest, Lock, and vendor/. +If the current project includes that dependency in its import graph, rm will fail unless -force is specified. +If -unused is provided, specs matches all dependencies in the Manifest that are not reachable by the import graph. +The -force and -unused flags cannot be combined (an error occurs). +During removal, dependencies that were only present because of the dependencies being removed are also removed. + +Note: this is a separate command to 'ensure' because we want the user to be explicit when making destructive changes. + +Flags: +-n Dry run, don’t actually remove anything +-unused Remove dependencies that are not used by this project +-force Remove dependency even if it is used by the project +-keep-source Do not remove source code + `, +} + +func runRemove(args []string) error { + p, err := depContext.loadProject("") + if err != nil { + return err + } + + sm, err := depContext.sourceManager() + if err != nil { + return err + } + defer sm.Release() + + cpr, err := depContext.splitAbsoluteProjectRoot(p.absroot) + if err != nil { + return errors.Wrap(err, "determineProjectRoot") + } + + pkgT, err := gps.ListPackages(p.absroot, cpr) + if err != nil { + return errors.Wrap(err, "gps.ListPackages") + } + + // get the list of packages + pd, err := getProjectData(pkgT, cpr, sm) + if err != nil { + return err + } + + for _, arg := range args { + /* + * - Remove package from manifest + * - if the package IS NOT being used, solving should do what we want + * - if the package IS being used: + * - Desired behavior: stop and tell the user, unless --force + * - Actual solver behavior: ? + */ + + if _, found := pd.dependencies[gps.ProjectRoot(arg)]; found { + //TODO: Tell the user where it is in use? + return fmt.Errorf("not removing '%s' because it is in use", arg) + } + delete(p.m.Dependencies, gps.ProjectRoot(arg)) + } + + params := gps.SolveParameters{ + RootDir: p.absroot, + RootPackageTree: pkgT, + Manifest: p.m, + Lock: p.l, + } + + if *verbose { + params.Trace = true + params.TraceLogger = log.New(os.Stderr, "", 0) + } + s, err := gps.Prepare(params, sm) + if err != nil { + return errors.Wrap(err, "prepare solver") + } + + soln, err := s.Solve() + if err != nil { + handleAllTheFailuresOfTheWorld(err) + return err + } + + p.l = lockFromInterface(soln) + + if err := writeFile(filepath.Join(p.absroot, manifestName), p.m); err != nil { + return errors.Wrap(err, "writeFile for manifest") + } + if err := writeFile(filepath.Join(p.absroot, lockName), p.l); err != nil { + return errors.Wrap(err, "writeFile for lock") + } + return nil +}