diff --git a/analyzer.go b/analyzer.go new file mode 100644 index 0000000000..326f01df0a --- /dev/null +++ b/analyzer.go @@ -0,0 +1,22 @@ +// 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 ( + "github.com/Masterminds/semver" + "github.com/sdboyer/gps" +) + +type analyzer struct{} + +func (a analyzer) DeriveManifestAndLock(path string, n gps.ProjectRoot) (gps.Manifest, gps.Lock, error) { + // TODO initial impl would just be looking for our own manifest and lock + return nil, nil, nil +} + +func (a analyzer) Info() (name string, version *semver.Version) { + v, _ := semver.NewVersion("v0.0.1") + return "example-analyzer", v +} diff --git a/lock.go b/lock.go index 2b8fb9b652..ebafa2df8a 100644 --- a/lock.go +++ b/lock.go @@ -13,7 +13,7 @@ import ( "github.com/sdboyer/gps" ) -type Lock struct { +type lock struct { Memo []byte P []gps.LockedProject } @@ -32,7 +32,7 @@ type lockedDep struct { Packages []string `json:"packages"` } -func ReadLock(r io.Reader) (*Lock, error) { +func readLock(r io.Reader) (*lock, error) { rl := rawLock{} err := json.NewDecoder(r).Decode(&rl) if err != nil { @@ -43,7 +43,7 @@ func ReadLock(r io.Reader) (*Lock, error) { if err != nil { return nil, fmt.Errorf("invalid hash digest in lock's memo field") } - l := &Lock{ + l := &lock{ Memo: b, P: make([]gps.LockedProject, len(rl.P)), } @@ -75,10 +75,10 @@ func ReadLock(r io.Reader) (*Lock, error) { return l, nil } -func (l *Lock) InputHash() []byte { +func (l *lock) InputHash() []byte { return l.Memo } -func (l *Lock) Projects() []gps.LockedProject { +func (l *lock) Projects() []gps.LockedProject { return l.P } diff --git a/lock_test.go b/lock_test.go index ddaa68f015..0619f6af85 100644 --- a/lock_test.go +++ b/lock_test.go @@ -38,20 +38,20 @@ func TestReadLock(t *testing.T) { ] }` - _, err := ReadLock(strings.NewReader(le)) + _, err := readLock(strings.NewReader(le)) if err == nil { t.Error("Reading lock with invalid props should have caused error, but did not") } else if !strings.Contains(err.Error(), "both a branch") { t.Errorf("Unexpected error %q; expected multiple version error", err) } - l, err := ReadLock(strings.NewReader(lg)) + l, err := readLock(strings.NewReader(lg)) if err != nil { t.Fatalf("Should have read Lock correctly, but got err %q", err) } b, _ := hex.DecodeString("2252a285ab27944a4d7adcba8dbd03980f59ba652f12db39fa93b927c345593e") - l2 := &Lock{ + l2 := &lock{ Memo: b, P: []gps.LockedProject{ gps.NewLockedProject( diff --git a/main.go b/main.go index 2b945006c5..6dca5e2fb3 100644 --- a/main.go +++ b/main.go @@ -9,10 +9,13 @@ import ( "fmt" "os" "path/filepath" + "strings" + + "github.com/sdboyer/gps" ) -const ManifestName = "manifest.json" -const LockName = "lock.json" +const manifestName = "manifest.json" +const lockName = "lock.json" func main() { flag.Parse() @@ -106,7 +109,7 @@ var initCmd = &command{ Write Manifest file in the root of the project directory. `, long: ` - Populates Manifest file with current deps of this project. + Populates Manifest file with current deps of this project. The specified version of each dependent repository is the version available in the user's workspaces (as specified by GOPATH). If the dependency is not present in any workspaces it is not be @@ -116,57 +119,6 @@ var initCmd = &command{ `, } -var statusCmd = &command{ - fn: noop, - name: "status", - short: `[flags] [packages] - Report the status of the current project's dependencies. - `, - long: ` - If no packages are specified, for each dependency: - - root import path - - (if present in lock) the currently selected version - - (else) that it's missing from the lock - - whether it's present in the vendor directory (or if it's in - workspace, if that's a thing?) - - the current aggregate constraints on that project (as specified by - the Manifest) - - if -u is specified, whether there are newer versions of this - dependency - - VCS state (uncommitted changes? pruned?) - - If packages are specified, or if -a is specified, - for each of those dependencies: - - (if present in lock) the currently selected version - - (else) that it's missing from the lock - - whether it's present in the vendor directory - - The set of possible versions for that project - - The upstream source URL(s) from which the project may be retrieved - - The type of upstream source (git, hg, bzr, svn, registry) - - Other versions that might work, given the current constraints - - The list of all projects that import the project within the current - depgraph - - The current constraint. If more than one project constrains it, both - the aggregate and the individual components (and which project provides - that constraint) are printed - - License information - - Package source location, if fetched from an alternate location - - Flags: - -json Output in json format - -f [template] Output in text/template format - - -old Only show out of date packages and the current version - -missing Only show missing packages. - -unused Only show unused packages. - -modified Only show modified packages. - - -dot Export dependency graph in GraphViz format - - The exit code of status is zero if all repositories are in a "good state". - `, -} - var getCmd = &command{ fn: noop, name: "get", @@ -179,7 +131,7 @@ var getCmd = &command{ -x dry run -f force the given package to be updated to the specified version - + Package specs: [@] @@ -203,7 +155,7 @@ func findProjectRoot(from string) (string, error) { var f func(string) (string, error) f = func(dir string) (string, error) { - fullpath := filepath.Join(dir, ManifestName) + fullpath := filepath.Join(dir, manifestName) if _, err := os.Stat(fullpath); err == nil { return dir, nil @@ -228,3 +180,83 @@ func findProjectRoot(from string) (string, error) { } return path, nil } + +type project struct { + // absroot is the absolute path to the root directory of the project. + absroot string + // importroot is the import path of the project's root directory. + importroot gps.ProjectRoot + m *manifest + l *lock +} + +// loadProject searches for a project root from the provided path, then loads +// the manifest and lock (if any) it finds there. +// +// If the provided path is empty, it will search from the path indicated by +// os.Getwd(). +func loadProject(path string) (*project, error) { + var err error + p := new(project) + + switch path { + case "": + p.absroot, err = findProjectRootFromWD() + default: + p.absroot, err = findProjectRoot(path) + } + + if err != nil { + return p, err + } + + gopath := os.Getenv("GOPATH") + var match bool + for _, gp := range filepath.SplitList(gopath) { + srcprefix := filepath.Join(gp, "src") + string(filepath.Separator) + if strings.HasPrefix(p.absroot, srcprefix) { + gopath = gp + match = true + // filepath.ToSlash because we're dealing with an import path now, + // not an fs path + p.importroot = gps.ProjectRoot(filepath.ToSlash(strings.TrimPrefix(p.absroot, srcprefix))) + break + } + } + if !match { + return nil, fmt.Errorf("could not determine project root - not on GOPATH") + } + + mp := filepath.Join(path, manifestName) + mf, err := os.Open(mp) + if err != nil { + // Should be impossible at this point for the manifest file not to + // exist, so this is some other kind of err + return nil, fmt.Errorf("could not open %s: %s", mp, err) + } + defer mf.Close() + + p.m, err = readManifest(mf) + if err != nil { + return nil, fmt.Errorf("error while parsing %s: %s", mp, err) + } + + lp := filepath.Join(path, lockName) + lf, err := os.Open(lp) + if err != nil { + if os.IsNotExist(err) { + // It's fine for the lock not to exist + return p, nil + } + // But if a lock does exist and we can't open it, that's a problem + return nil, fmt.Errorf("could not open %s: %s", lp, err) + } + + defer lf.Close() + p.l, err = readLock(lf) + if err != nil { + return nil, fmt.Errorf("error while parsing %s: %s", lp, err) + } + + return p, nil +} diff --git a/main_test.go b/main_test.go index fc65cb8a2e..d5fdde315c 100644 --- a/main_test.go +++ b/main_test.go @@ -35,7 +35,7 @@ func TestFindRoot(t *testing.T) { t.Errorf("findProjectRoot on nonexistent subdir should still work and give %s, got %s", expect, got3) } - got4, err := findProjectRoot(filepath.Join(expect, ManifestName)) + got4, err := findProjectRoot(filepath.Join(expect, manifestName)) if err == nil { t.Errorf("Should have err'd when trying subdir of file, but returned %s", got4) } diff --git a/manifest.go b/manifest.go index 044d5ea74e..830cb2d50a 100644 --- a/manifest.go +++ b/manifest.go @@ -12,7 +12,7 @@ import ( "github.com/sdboyer/gps" ) -type Manifest struct { +type manifest struct { Dependencies gps.ProjectConstraints Ovr gps.ProjectConstraints Ignores []string @@ -31,14 +31,14 @@ type possibleProps struct { NetworkName string `json:"network_name"` } -func ReadManifest(r io.Reader) (*Manifest, error) { +func readManifest(r io.Reader) (*manifest, error) { rm := rawManifest{} err := json.NewDecoder(r).Decode(&rm) if err != nil { return nil, err } - m := &Manifest{ + m := &manifest{ Dependencies: make(gps.ProjectConstraints, len(rm.Dependencies)), Ovr: make(gps.ProjectConstraints, len(rm.Overrides)), Ignores: rm.Ignores, @@ -90,20 +90,20 @@ func toProps(n string, p possibleProps) (pp gps.ProjectProperties, err error) { return pp, nil } -func (m *Manifest) DependencyConstraints() gps.ProjectConstraints { +func (m *manifest) DependencyConstraints() gps.ProjectConstraints { return m.Dependencies } -func (m *Manifest) TestDependencyConstraints() gps.ProjectConstraints { +func (m *manifest) TestDependencyConstraints() gps.ProjectConstraints { // TODO decide whether we're going to incorporate this or not return nil } -func (m *Manifest) Overrides() gps.ProjectConstraints { +func (m *manifest) Overrides() gps.ProjectConstraints { return m.Ovr } -func (m *Manifest) IgnorePackages() map[string]bool { +func (m *manifest) IgnorePackages() map[string]bool { if len(m.Ignores) == 0 { return nil } diff --git a/sm.go b/sm.go new file mode 100644 index 0000000000..ae4218f5e7 --- /dev/null +++ b/sm.go @@ -0,0 +1,24 @@ +// 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" + "os" + "path/filepath" + + "github.com/sdboyer/gps" +) + +func getSourceManager() (*gps.SourceMgr, error) { + gopath := os.Getenv("GOPATH") + if gopath == "" { + return nil, fmt.Errorf("GOPATH is not set") + } + // Use the first entry in GOPATH for the depcache + first := filepath.SplitList(gopath)[0] + + return gps.NewSourceManager(analyzer{}, filepath.Join(first, "depcache")) +} diff --git a/status.go b/status.go new file mode 100644 index 0000000000..fc0aec7637 --- /dev/null +++ b/status.go @@ -0,0 +1,222 @@ +// 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 ( + "bytes" + "fmt" + "os" + "text/tabwriter" + + "github.com/sdboyer/gps" +) + +var statusCmd = &command{ + fn: runStatus, + name: "status", + short: `[flags] [packages] + Report the status of the current project's dependencies. + `, + long: ` + If no packages are specified, for each dependency: + - root import path + - (if present in lock) the currently selected version + - (else) that it's missing from the lock + - whether it's present in the vendor directory (or if it's in + workspace, if that's a thing?) + - the current aggregate constraints on that project (as specified by + the Manifest) + - if -u is specified, whether there are newer versions of this + dependency + + If packages are specified, or if -a is specified, + for each of those dependencies: + - (if present in lock) the currently selected version + - (else) that it's missing from the lock + - whether it's present in the vendor directory + - The set of possible versions for that project + - The upstream source URL(s) from which the project may be retrieved + - The type of upstream source (git, hg, bzr, svn, registry) + - Other versions that might work, given the current constraints + - The list of all projects that import the project within the current + depgraph + - The current constraint. If more than one project constrains it, both + the aggregate and the individual components (and which project provides + that constraint) are printed + - License information + - Package source location, if fetched from an alternate location + + Flags: + -json Output in json format + -f [template] Output in text/template format + + -old Only show out of date packages and the current version + -missing Only show missing packages. + -unused Only show unused packages. + -modified Only show modified packages. + + -dot Export dependency graph in GraphViz format + + The exit code of status is zero if all repositories are in a "good state". + `, +} + +// BasicStatus contains all the information reported about a single dependency +// in the summary/list status output mode. +type BasicStatus struct { + ProjectRoot string + Constraint gps.Constraint + Version gps.UnpairedVersion + Revision gps.Revision + Latest gps.Version + PackageCount int +} + +func runStatus(args []string) error { + p, err := loadProject("") + if err != nil { + return err + } + + sm, err := getSourceManager() + if err != nil { + return err + } + defer sm.Release() + + if len(args) == 0 { + return runStatusAll(p, sm) + } + return runStatusDetailed(p, sm, args) +} + +func runStatusAll(p *project, sm *gps.SourceMgr) error { + if p.l == nil { + // TODO if we have no lock file, do...other stuff + return nil + } + + // In the background, warm caches of version lists for all the projects in + // the lock. The SourceMgr coordinates access to this information - if the + // main goroutine asks for the version list while the background goroutine's + // request is in flight (or vice versa), both calls are folded together and + // are fulfilled from the same network response, and the same on-disk + // repository cache. + for _, proj := range p.l.Projects() { + id := proj.Ident() + go func() { + sm.ListVersions(id) + }() + } + + // While the network churns on ListVersions() requests, statically analyze + // code from the current project. + ptree, err := gps.ListPackages(p.absroot, string(p.importroot)) + + // Set up a solver in order to check the InputHash. + params := gps.SolveParameters{ + RootDir: p.absroot, + RootPackageTree: ptree, + Manifest: p.m, + // Locks aren't a part of the input hash check, so we can omit it. + } + + s, err := gps.Prepare(params, sm) + if err != nil { + return fmt.Errorf("could not set up solver for input hashing, err: %s", err) + } + + cm := collectConstraints(ptree, p, sm) + tw := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', 0) + // Print header row + fmt.Fprintf(tw, "Project\tConstraint\tVersion\tRevision\tLatest\tPkgs Used\t\n") + + if bytes.Equal(s.HashInputs(), p.l.Memo) { + // If these are equal, we're guaranteed that the lock is a transitively + // complete picture of all deps. That eliminates the need for at least + // some checks. + for _, proj := range p.l.Projects() { + bs := BasicStatus{ + ProjectRoot: string(proj.Ident().ProjectRoot), + PackageCount: len(proj.Packages()), + } + + // Split apart the version from the lock into its constituent parts + switch tv := proj.Version().(type) { + case gps.UnpairedVersion: + bs.Version = tv + case gps.Revision: + bs.Revision = tv + case gps.PairedVersion: + bs.Version = tv.Unpair() + bs.Revision = tv.Underlying() + } + + // Check if the manifest has an override for this project. If so, + // set that as the constraint. + if pp, has := p.m.Ovr[proj.Ident().ProjectRoot]; has && pp.Constraint != nil { + // TODO note somehow that it's overridden + bs.Constraint = pp.Constraint + } else { + bs.Constraint = gps.Any() + for _, c := range cm[bs.ProjectRoot] { + bs.Constraint = c.Intersect(bs.Constraint) + } + } + + // Only if we have a non-rev and non-plain version do/can we display + // anything wrt the version's updateability. + if bs.Version != nil && bs.Version.Type() != "version" { + c, has := p.m.Dependencies[proj.Ident().ProjectRoot] + if !has { + c.Constraint = gps.Any() + } + + vl, err := sm.ListVersions(proj.Ident()) + if err != nil { + gps.SortForUpgrade(vl) + + for _, v := range vl { + // Because we've sorted the version list for upgrade, the + // first version we encounter that matches our constraint + // will be what we want + if c.Constraint.Matches(v) { + bs.Latest = v + break + } + } + } + } + + fmt.Fprintf(tw, + "%s\t%s\t%s\t%s\t%s\t%s\t\n", + bs.ProjectRoot, + bs.Constraint, + bs.Version, + string(bs.Revision)[:7], + bs.Latest, + bs.PackageCount, + ) + } + } else { + // Not equal - the lock may or may not be a complete picture, and even + // if it does have all the deps, it may not be a valid set of + // constraints. + // + // TODO + } + + return nil +} + +func runStatusDetailed(p *project, sm *gps.SourceMgr, args []string) error { + // TODO + return fmt.Errorf("not implemented") +} + +func collectConstraints(ptree gps.PackageTree, p *project, sm *gps.SourceMgr) map[string][]gps.Constraint { + // TODO + return map[string][]gps.Constraint{} +}