diff --git a/ensure.go b/ensure.go index 3b95843f57..347e4b2350 100644 --- a/ensure.go +++ b/ensure.go @@ -10,97 +10,110 @@ import ( "flag" "fmt" "io" + "io/ioutil" "log" "os" "path/filepath" "strconv" "strings" - "io/ioutil" - "github.com/pkg/errors" "github.com/sdboyer/gps" ) -var ensureCmd = &command{ - fn: runEnsure, - name: "ensure", - flag: flag.NewFlagSet("", flag.ExitOnError), - short: `Ensure a dependency is the vendor directory of the current project`, - long: `Run it when -To ensure a new dependency is in your project. -To ensure a dependency is updated. -To the latest version that satisfies constraints. -To a specific version or different constraint. -(With no arguments) To ensure that you have all the dependencies specified by your Manifest + lockfile. - - -What it does -Download code, placing in the vendor/ directory of the project. Only the packages that are actually used by the current project and its dependencies are included. -Any authentication, proxy settings, or other parameters regarding communicating with external repositories is the responsibility of the underlying VCS tools. -Resolve version constraints -If the set of constraints are not solvable, print an error -Collapse any vendor folders in the downloaded code and its transient deps to the root. -Includes dependencies required by the current project’s tests. There are arguments both for and against including the deps of tests for any transitive deps. Defer on deciding this for now. -Copy the relevant versions of the code to the current project’s vendor directory. -The source of that code is implementation dependant. Probably some kind of local cache of the VCS data (not a GOPATH/workspace). -Write Manifest (if changed) and Lockfile -Print what changed - - -Flags: - -update update all packages - -n dry run - -override specify an override constraints for package(s) - - -Package specs: +const ensureShortHelp = `Ensure a dependency is vendored in the project` +const ensureLongHelp = ` + +Ensure is used to fetch project dependencies into the vendor folder, as well as +to set version constraints for specific dependencies. It takes user input, +solves the updated dependency graph of the project, writes any changes to the +manifest and lock file, and downloads dependencies to the vendor folder. + +Package spec: + [:alt location][@] Examples: -Fetch/update github.com/heroku/rollrus to latest version, including transitive dependencies (ensuring it matches the constraints of rollrus, or—if not contrained—their latest versions): - $ dep ensure github.com/heroku/rollrus -Same dep, but choose any minor patch release in the 0.9.X series, setting the constraint. If another constraint exists that constraint is changed to ~0.9.0: - $ dep ensure github.com/heroku/rollrus@~0.9.0 -Same dep, but choose any release >= 0.9.1 and < 1.0.0, setting/changing constraints: - $ dep ensure github.com/heroku/rollrus@^0.9.1 -Same dep, but updating to 1.0.X: - $ dep ensure github.com/heroku/rollrus@~1.0.0 -Same dep, but fetching from a different location: - $ dep ensure github.com/heroku/rollrus:git.example.com/foo/bar -Same dep, but check out a specific version or range without updating the Manifest and update the Lockfile. This will fail if the specified version does not satisfy any existing constraints: - $ dep ensure github.com/heroku/rollrus==1.2.3 # 1.2.3 specifically - $ dep ensure github.com/heroku/rollrus=^1.2.0 # >= 1.2.0 < 2.0.0 -Override any declared dependency range of 'github.com/foo/bar' to have the range of '^0.9.1'. This applies transitively: - $ dep ensure -override github.com/foo/bar@^0.9.1 - - -Transitive deps are ensured based on constraints in the local Manifest if they exist, then constraints in the dependency’s Manifest file. A lack of constraints defaults to the latest version, eg "^2". - - -For a description of the version specifier string, see this handy guide from crates.io. We are going to defer on making a final decision about this syntax until we have more experience with it in practice. - `, -} -// stringSlice is a slice of strings -type stringSlice []string + dep ensure Populate vendor from existing manifest and lock + dep ensure github.com/heroku/rollrus@^0.9.1 Update a specific dependency to a specific version + dep ensure -update Update all dependencies to latest permitted versions -// implement the flag interface for stringSlice -func (s *stringSlice) String() string { - return fmt.Sprintf("%s", *s) -} -func (s *stringSlice) Set(value string) error { - *s = append(*s, value) - return nil -} +For more detailed usage examples, see dep ensure -examples. +` +const ensureExamples = ` +dep ensure + + Solve the project's dependency graph, and download all dependencies to the + vendor folder. If a dependency is in the lock file, use the version + specified there. Otherwise, use the most recent version that can satisfy the + constraints in the manifest file. + +dep ensure -update + + Update all dependencies to the latest version allowed by the manifest, ignoring + any versions specified in the lock file. Update the lock file with any + changes. + +dep ensure github.com/heroku/rollrus + + Update a specific dependency to the latest version allowed by the manifest, + including all of its transitive dependencies. + +dep ensure github.com/heroku/rollrus@~0.9.0 + + Same as above, but choose any release matching 0.9.x, preferring latest. If + a constraint was previously set in the manifest, this resets it. + +dep ensure github.com/heroku/rollrus@^0.9.1 + + Same as above, but choose any release >= 0.9.1, < 1.0.0. This form of + constraint strikes a good balance of safety and flexibility, and should be + preferred for libraries. -var overrides stringSlice +dep ensure github.com/heroku/rollrus:git.internal.com/foo/bar -func init() { - ensureCmd.flag.Var(&overrides, "override", "Interpret specified constraint(s) as override(s) rather than normal constraints") + Fetch the dependency from a different location. + +dep ensure github.com/heroku/rollrus==1.2.3 # 1.2.3 exactly +dep ensure github.com/heroku/rollrus=^1.2.0 # >= 1.2.0, < 2.0.0 + + Fetch the dependency at a specific version or range, and update the lock + file, but don't update the manifest file. Will fail if the specified version + doesn't satisfy the constraint in the manifest file. + +dep ensure -override github.com/heroku/rollrus@^0.9.1 + + Forcefully and transitively override any constraint for this dependency. + This can inadvertantly make your dependency graph unsolvable; use sparingly. + +` + +func (cmd *ensureCommand) Name() string { return "ensure" } +func (cmd *ensureCommand) Args() string { return "[spec...]" } +func (cmd *ensureCommand) ShortHelp() string { return ensureShortHelp } +func (cmd *ensureCommand) LongHelp() string { return ensureLongHelp } + +func (cmd *ensureCommand) Register(fs *flag.FlagSet) { + fs.BoolVar(&cmd.examples, "examples", false, "print detailed usage examples") + fs.BoolVar(&cmd.update, "update", false, "ensure all dependencies are at the latest version allowed by the manifest") + fs.BoolVar(&cmd.dryRun, "n", false, "dry run, don't actually ensure anything") + fs.Var(&cmd.overrides, "override", "specify an override constraint spec (repeatable)") +} + +type ensureCommand struct { + examples bool + update bool + dryRun bool + overrides stringSlice } -func runEnsure(args []string) error { +func (cmd *ensureCommand) Run(args []string) error { + if cmd.examples { + fmt.Fprintln(os.Stderr, strings.TrimSpace(ensureExamples)) + return nil + } + p, err := depContext.loadProject("") if err != nil { return err @@ -132,7 +145,7 @@ func runEnsure(args []string) error { } } - for _, ovr := range overrides { + for _, ovr := range cmd.overrides { constraint, err := getProjectConstraint(ovr, sm) if err != nil { errs = append(errs, err) @@ -236,6 +249,20 @@ func runEnsure(args []string) error { return nil } +type stringSlice []string + +func (s *stringSlice) String() string { + if len(*s) == 0 { + return "" + } + return strings.Join(*s, ", ") +} + +func (s *stringSlice) Set(value string) error { + *s = append(*s, value) + return nil +} + func getProjectConstraint(arg string, sm *gps.SourceMgr) (gps.ProjectConstraint, error) { constraint := gps.ProjectConstraint{} diff --git a/init.go b/init.go index 1a89d1c761..776806b1d9 100644 --- a/init.go +++ b/init.go @@ -6,6 +6,7 @@ package main import ( "encoding/json" + "flag" "fmt" "log" "os" @@ -16,43 +17,41 @@ import ( "github.com/sdboyer/gps" ) -var initCmd = &command{ - fn: runInit, - name: "init", - short: `Write manifest and lock files for the current project`, - long: `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 - included in the Manifest. - Writes Lock file(?) - Creates vendor/ directory(?) - - Notes from DOC: - Reads existing dependency information written by other tools. - Noting any information that is lost (unsupported features, etc). - This functionality will be removed after a transition period (1 year?). - Write Manifest file in the root of the project directory. - * 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 (including vendor/ directories, if present). - If the dependency is not present in any workspaces it will not be included in the Manifest. A warning will be issued for these dependencies. - Creates vendor/ directory (if it does not exist) - Copies the project’s dependencies from the workspace to the vendor/ directory (if they’re not already there). - Writes a Lockfile in the root of the project directory. - Invoke “dep status”.`, -} +const initShortHelp = `Write manifest and lock files for the project` +const initLongHelp = ` +Initialize the project at filepath root by parsing its dependencies and writing +manifest and lock files. If root isn't specified, use the current directory. + +The version of each dependency will reflect the current state of the GOPATH. If +a dependency doesn't exist in the GOPATH, it won't be written to the manifest, +but it will be solved-for, and will appear in the lock. + +Note: init may use the network to solve the dependency graph. + +Note: init does NOT vendor dependencies. See dep ensure. +` + +func (cmd *initCommand) Name() string { return "init" } +func (cmd *initCommand) Args() string { return "[root]" } +func (cmd *initCommand) ShortHelp() string { return initShortHelp } +func (cmd *initCommand) LongHelp() string { return initLongHelp } -func runInit(args []string) error { +func (cmd *initCommand) Register(fs *flag.FlagSet) {} + +type initCommand struct{} + +func (cmd *initCommand) Run(args []string) error { if len(args) > 1 { - return fmt.Errorf("Too many args: %d", len(args)) + return errors.Errorf("too many args (%d)", len(args)) } + var root string - var err error - if len(args) == 0 { - root, err = os.Getwd() + if len(args) <= 0 { + wd, err := os.Getwd() if err != nil { - return errors.Wrap(err, "os.Getwd") + return err } + root = wd } else { root = args[0] } @@ -65,7 +64,7 @@ func runInit(args []string) error { return err } if mok { - return fmt.Errorf("Manifest file %q already exists", mf) + return fmt.Errorf("manifest file %q already exists", mf) } // Manifest file does not exist. diff --git a/main.go b/main.go index d7bbc2ce62..c10c01323a 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ package main import ( - "errors" + "bytes" "flag" "fmt" "os" @@ -13,6 +13,7 @@ import ( "strings" "text/tabwriter" + "github.com/pkg/errors" "github.com/sdboyer/gps" ) @@ -26,133 +27,119 @@ var ( verbose = flag.Bool("v", false, "enable verbose logging") ) -func main() { - flag.Usage = func() { - help(nil) - } - flag.Parse() +type command interface { + Name() string // "foobar" + Args() string // " [quux...]" + ShortHelp() string // "Foo the first bar" + LongHelp() string // "Foo the first bar meeting the following conditions..." + Register(*flag.FlagSet) // command-specific flags + Run([]string) error +} - // newContext() will set the GOPATH for us to use for various functions. - var err error - depContext, err = newContext() +func main() { + // Set up the dep context. + // TODO(pb): can this be deglobalized, pretty please? + dc, err := newContext() if err != nil { fmt.Fprint(os.Stderr, err.Error()) os.Exit(1) } - - do := flag.Arg(0) - var args []string - if do == "" { - do = "help" - } else { - args = flag.Args() + depContext = dc + + // Build the list of available commands. + commands := []command{ + &initCommand{}, + &statusCommand{}, + &ensureCommand{}, + &removeCommand{}, } - for _, cmd := range commands { - if do != cmd.name { - continue - } - if cmd.flag != nil { - cmd.flag.Usage = func() { cmd.Usage() } - err = cmd.flag.Parse(args[1:]) - if err != nil { - fmt.Fprint(os.Stderr, err.Error()) - os.Exit(1) - } - args = cmd.flag.Args() - } else { - if len(args) > 0 { - args = args[1:] - } + usage := func() { + fmt.Fprintln(os.Stderr, "Usage: dep ") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, "Commands:") + fmt.Fprintln(os.Stderr) + w := tabwriter.NewWriter(os.Stderr, 0, 4, 2, ' ', 0) + for _, command := range commands { + fmt.Fprintf(w, "\t%s\t%s\n", command.Name(), command.ShortHelp()) } + w.Flush() + fmt.Fprintln(os.Stderr) + } - if err := cmd.fn(args); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - os.Exit(0) + if len(os.Args) <= 1 || len(os.Args) == 2 && strings.ToLower(os.Args[1]) == "help" { + usage() + os.Exit(1) } - fmt.Fprintf(os.Stderr, "unknown command: %q", flag.Arg(0)) - help(nil) - os.Exit(2) -} + for _, command := range commands { + if name := command.Name(); os.Args[1] == name { + // Build flag set with global flags in there. + // TODO(pb): can we deglobalize verbose, pretty please? + fs := flag.NewFlagSet(name, flag.ExitOnError) + fs.BoolVar(verbose, "v", false, "enable verbose logging") -type command struct { - fn func(args []string) error - name string - short string - long string - flag *flag.FlagSet -} + // Register the subcommand flags in there, too. + command.Register(fs) -func (c *command) Usage() { - fmt.Fprintf(os.Stderr, "usage: %s\n\n", c.short) - fmt.Fprintf(os.Stderr, "%s\n", strings.TrimSpace(c.long)) - os.Exit(2) -} + // Override the usage text to something nicer. + resetUsage(fs, command.Name(), command.Args(), command.LongHelp()) -var commands = []*command{ - initCmd, - statusCmd, - ensureCmd, - removeCmd, - // help added here at init time. -} + // Parse the flags the user gave us. + if err := fs.Parse(os.Args[2:]); err != nil { + fs.Usage() + os.Exit(1) + } -var ( - errProjectNotFound = errors.New("no project could be found") -) + // Run the command with the post-flag-processing args. + if err := command.Run(fs.Args()); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } -func init() { - // Defeat circular declarations by appending - // this to the list at init time. - commands = append(commands, &command{ - fn: help, - name: "help", - short: `Show documentation for the dep tool or the specified command`, - }) + // Easy peasy livin' breezy. + return + } + } + + fmt.Fprintf(os.Stderr, "%s: no such command\n", os.Args[1]) + usage() + os.Exit(1) } -func help(args []string) error { - if len(args) > 1 { - // If they're misusing help, show them how it's done. - args = []string{"help"} - } - if len(args) == 0 { - // Show short usage for all commands. - fmt.Println("usage: dep [arguments]") - fmt.Println() - fmt.Println("Available commands:") - fmt.Println() - w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) - for _, cmd := range commands { - fmt.Fprintf(w, "\t%s\t%s\n", cmd.name, cmd.short) +func resetUsage(fs *flag.FlagSet, name, args, longHelp string) { + var ( + hasFlags bool + flagBlock bytes.Buffer + flagWriter = tabwriter.NewWriter(&flagBlock, 0, 4, 2, ' ', 0) + ) + fs.VisitAll(func(f *flag.Flag) { + hasFlags = true + // Default-empty string vars should read "(default: )" + // rather than the comparatively ugly "(default: )". + defValue := f.DefValue + if defValue == "" { + defValue = "" } - w.Flush() - fmt.Println() - return nil - } - // Show full help for a specific command. - for _, cmd := range commands { - if cmd.name != args[0] { - continue + fmt.Fprintf(flagWriter, "\t-%s\t%s (default: %s)\n", f.Name, f.Usage, defValue) + }) + flagWriter.Flush() + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: dep %s %s\n", name, args) + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, strings.TrimSpace(longHelp)) + fmt.Fprintln(os.Stderr) + if hasFlags { + fmt.Fprintln(os.Stderr, "Flags:") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, flagBlock.String()) } - fmt.Printf("usage: dep %s\n", cmd.name) - fmt.Println() - fmt.Printf("%s\n", cmd.short) - fmt.Println() - fmt.Println(cmd.long) - fmt.Println() - return nil } - return fmt.Errorf("unknown command: %q", args[0]) } -func noop(args []string) error { - fmt.Println("noop called with flags:", args) - return nil -} +var ( + errProjectNotFound = errors.New("no project could be found") +) func findProjectRootFromWD() (string, error) { path, err := os.Getwd() diff --git a/remove.go b/remove.go index ea868918f1..bca1dcd6fa 100644 --- a/remove.go +++ b/remove.go @@ -5,6 +5,7 @@ package main import ( + "flag" "fmt" "log" "os" @@ -14,31 +15,33 @@ import ( "github.com/sdboyer/gps" ) -var removeCmd = &command{ - fn: runRemove, - name: "rm", - short: `Remove one or more dependencies from the current project`, - 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`, +const removeShortHelp = `Remove a dependency from the project` +const removeLongHelp = ` +Remove a dependency from the project's manifest file, lock file, and vendor +folder. If the project includes that dependency in its import graph, remove will +fail unless -force is specified. +` + +func (cmd *removeCommand) Name() string { return "remove" } +func (cmd *removeCommand) Args() string { return "[spec...]" } +func (cmd *removeCommand) ShortHelp() string { return removeShortHelp } +func (cmd *removeCommand) LongHelp() string { return removeLongHelp } + +func (cmd *removeCommand) Register(fs *flag.FlagSet) { + fs.BoolVar(&cmd.dryRun, "n", false, "dry run, don't actually remove anything") + fs.BoolVar(&cmd.unused, "unused", false, "remove all dependencies that aren't imported by the project") + fs.BoolVar(&cmd.force, "force", false, "remove the given dependencies even if they are imported by the project") + fs.BoolVar(&cmd.keepSource, "keep-source", false, "don't remove source code") } -func runRemove(args []string) error { +type removeCommand struct { + dryRun bool + unused bool + force bool + keepSource bool +} + +func (cmd *removeCommand) Run(args []string) error { p, err := depContext.loadProject("") if err != nil { return err diff --git a/status.go b/status.go index 86071c5c5e..262bd33ee8 100644 --- a/status.go +++ b/status.go @@ -6,6 +6,7 @@ package main import ( "bytes" + "flag" "fmt" "log" "os" @@ -15,67 +16,54 @@ import ( "github.com/sdboyer/gps" ) -var statusCmd = &command{ - fn: runStatus, - name: "status", - short: `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".`, +const statusShortHelp = `Report the status of the project's dependencies` +const statusLongHelp = ` +With no arguments, print the status of each dependency of the project. + + PROJECT The import path of the dependency + CONSTRAINT The version constraint for the dependency, from the manifest + VERSION The version chosen for the dependency, from the lock + REVISION The VCS revision of the chosen version + LATEST The latest VCS revision available for the dependency + PKGS USED The number of packages (dependencies) used by this package + +With one or more explicitly specified packages, or with the -detailed flag, +print an extended status output for each dependency of the project. + + TODO Another column description. + FOOBAR Another column description. + +Status returns exit code zero if all dependencies are in a "good state". +` + +func (cmd *statusCommand) Name() string { return "status" } +func (cmd *statusCommand) Args() string { return "[package...]" } +func (cmd *statusCommand) ShortHelp() string { return statusShortHelp } +func (cmd *statusCommand) LongHelp() string { return statusLongHelp } + +func (cmd *statusCommand) Register(fs *flag.FlagSet) { + fs.BoolVar(&cmd.detailed, "detailed", false, "report more detailed status") + fs.BoolVar(&cmd.json, "json", false, "output in JSON format") + fs.StringVar(&cmd.template, "f", "", "output in text/template format") + fs.BoolVar(&cmd.dot, "dot", false, "output the dependency graph in GraphViz format") + fs.BoolVar(&cmd.old, "old", false, "only show out-of-date dependencies") + fs.BoolVar(&cmd.missing, "missing", false, "only show missing dependencies") + fs.BoolVar(&cmd.unused, "unused", false, "only show unused dependencies") + fs.BoolVar(&cmd.modified, "modified", false, "only show modified dependencies") } -// 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 +type statusCommand struct { + detailed bool + json bool + template string + dot bool + old bool + missing bool + unused bool + modified bool } -type MissingStatus struct { - ProjectRoot string - MissingPackages string -} - -func runStatus(args []string) error { +func (cmd *statusCommand) Run(args []string) error { p, err := depContext.loadProject("") if err != nil { return err @@ -87,10 +75,26 @@ func runStatus(args []string) error { } defer sm.Release() - if len(args) == 0 { - return runStatusAll(p, sm) + if cmd.detailed { + return runStatusDetailed(p, sm, args) } - return runStatusDetailed(p, sm, args) + return runStatusAll(p, sm) +} + +// 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 +} + +type MissingStatus struct { + ProjectRoot string + MissingPackages string } func runStatusAll(p *project, sm *gps.SourceMgr) error {