diff --git a/Makefile b/Makefile index 58889afe..9b15929e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ NAME := volt SRC := $(shell find . -type d -name 'vendor' -prune -o -type f -name '*.go' -print) -VERSION := $(shell sed -n -E 's/var voltVersion = "([^"]+)"/\1/p' subcmd/version.go) +VERSION := $(shell sed -n -E 's/var VoltVersion = "([^"]+)"/\1/p' usecase/version.go) RELEASE_LDFLAGS := -s -w -extldflags '-static' RELEASE_OS := linux windows darwin RELEASE_ARCH := amd64 386 diff --git a/_docs/layer.md b/_docs/layer.md new file mode 100644 index 00000000..12052ac5 --- /dev/null +++ b/_docs/layer.md @@ -0,0 +1,20 @@ + +## Layered architecture + +The volt commands like `volt get` which may modify lock.json, config.toml, +filesystem, are executed in several steps: + +1. (UI layer): Passes subcommand arguments, lock.json & config.toml structure to Gateway layer +2. (Gateway layer): Invokes usecase(s). This layer cannot touch filesystem, do network requests, because it makes unit testing difficult +3. (Usecase layer): Modify files, do network requests, and other business logic + +Below is the dependency graph: + +``` +UI --> Gateway --> Usecase +``` + +* UI only depends Gateway +* Gateway doesn't know UI +* Gateway only depends Usecase +* Usecase doesn't know Gateway diff --git a/subcmd/build.go b/gateway/build.go similarity index 91% rename from subcmd/build.go rename to gateway/build.go index 0b871d60..fceed85d 100644 --- a/subcmd/build.go +++ b/gateway/build.go @@ -1,12 +1,12 @@ -package subcmd +package gateway import ( "flag" "fmt" "os" + "github.com/vim-volt/volt/gateway/builder" "github.com/vim-volt/volt/logger" - "github.com/vim-volt/volt/subcmd/builder" "github.com/vim-volt/volt/transaction" ) @@ -53,10 +53,10 @@ Description return fs } -func (cmd *buildCmd) Run(args []string) *Error { +func (cmd *buildCmd) Run(cmdctx *CmdContext) *Error { // Parse args fs := cmd.FlagSet() - fs.Parse(args) + fs.Parse(cmdctx.Args) if cmd.helped { return nil } @@ -69,7 +69,7 @@ func (cmd *buildCmd) Run(args []string) *Error { } defer transaction.Remove() - err = builder.Build(cmd.full) + err = builder.Build(cmd.full, cmdctx.Config, cmdctx.LockJSON) if err != nil { logger.Error() return &Error{Code: 12, Msg: "Failed to build: " + err.Error()} diff --git a/subcmd/build_test.go b/gateway/build_test.go similarity index 99% rename from subcmd/build_test.go rename to gateway/build_test.go index 77b95172..1dcf2a9a 100644 --- a/subcmd/build_test.go +++ b/gateway/build_test.go @@ -1,4 +1,4 @@ -package subcmd +package gateway import ( "bytes" @@ -13,10 +13,10 @@ import ( "github.com/haya14busa/go-vimlparser" "github.com/vim-volt/volt/config" "github.com/vim-volt/volt/fileutil" + "github.com/vim-volt/volt/gateway/builder" "github.com/vim-volt/volt/internal/testutil" "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/pathutil" - "github.com/vim-volt/volt/subcmd/builder" ) // Checks: diff --git a/subcmd/builder/base.go b/gateway/builder/base.go similarity index 98% rename from subcmd/builder/base.go rename to gateway/builder/base.go index 6d4b63fa..612be43e 100644 --- a/subcmd/builder/base.go +++ b/gateway/builder/base.go @@ -12,10 +12,10 @@ import ( "github.com/hashicorp/go-multierror" "github.com/vim-volt/volt/fileutil" + "github.com/vim-volt/volt/gateway/buildinfo" "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" "github.com/vim-volt/volt/pathutil" - "github.com/vim-volt/volt/subcmd/buildinfo" ) // BaseBuilder is a base struct which all builders must implement diff --git a/subcmd/builder/builder.go b/gateway/builder/builder.go similarity index 88% rename from subcmd/builder/builder.go rename to gateway/builder/builder.go index adc557a6..9d9a0bd9 100644 --- a/subcmd/builder/builder.go +++ b/gateway/builder/builder.go @@ -1,30 +1,26 @@ package builder import ( - "github.com/pkg/errors" "os" + "github.com/pkg/errors" + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/gateway/buildinfo" + "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" "github.com/vim-volt/volt/pathutil" - "github.com/vim-volt/volt/subcmd/buildinfo" ) // Builder creates/updates ~/.vim/pack/volt directory type Builder interface { - Build(buildInfo *buildinfo.BuildInfo, buildReposMap map[pathutil.ReposPath]*buildinfo.Repos) error + Build(buildInfo *buildinfo.BuildInfo, buildReposMap map[pathutil.ReposPath]*buildinfo.Repos, lockJSON *lockjson.LockJSON) error } const currentBuildInfoVersion = 2 // Build creates/updates ~/.vim/pack/volt directory -func Build(full bool) error { - // Read config.toml - cfg, err := config.Read() - if err != nil { - return errors.Wrap(err, "could not read config.toml") - } - +func Build(full bool, cfg *config.Config, lockJSON *lockjson.LockJSON) error { // Get builder blder, err := getBuilder(cfg.Build.Strategy) if err != nil { @@ -75,7 +71,7 @@ func Build(full bool) error { } } - return blder.Build(buildInfo, buildReposMap) + return blder.Build(buildInfo, buildReposMap, lockJSON) } func getBuilder(strategy string) (Builder, error) { diff --git a/subcmd/builder/copy.go b/gateway/builder/copy.go similarity index 98% rename from subcmd/builder/copy.go rename to gateway/builder/copy.go index 5d079a2d..96b0bbfd 100644 --- a/subcmd/builder/copy.go +++ b/gateway/builder/copy.go @@ -10,12 +10,12 @@ import ( "github.com/hashicorp/go-multierror" "github.com/vim-volt/volt/fileutil" + "github.com/vim-volt/volt/gateway/buildinfo" "github.com/vim-volt/volt/gitutil" "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" "github.com/vim-volt/volt/pathutil" "github.com/vim-volt/volt/plugconf" - "github.com/vim-volt/volt/subcmd/buildinfo" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/object" @@ -25,19 +25,13 @@ type copyBuilder struct { BaseBuilder } -func (builder *copyBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposMap map[pathutil.ReposPath]*buildinfo.Repos) error { +func (builder *copyBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposMap map[pathutil.ReposPath]*buildinfo.Repos, lockJSON *lockjson.LockJSON) error { // Exit if vim executable was not found in PATH vimExePath, err := pathutil.VimExecutable() if err != nil { return err } - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.New("could not read lock.json: " + err.Error()) - } - // Get current profile's repos list reposList, err := lockJSON.GetCurrentReposList() if err != nil { diff --git a/subcmd/builder/symlink.go b/gateway/builder/symlink.go similarity index 95% rename from subcmd/builder/symlink.go rename to gateway/builder/symlink.go index 45c079d4..13e0e488 100644 --- a/subcmd/builder/symlink.go +++ b/gateway/builder/symlink.go @@ -11,12 +11,12 @@ import ( "gopkg.in/src-d/go-git.v4" + "github.com/vim-volt/volt/gateway/buildinfo" "github.com/vim-volt/volt/gitutil" "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" "github.com/vim-volt/volt/pathutil" "github.com/vim-volt/volt/plugconf" - "github.com/vim-volt/volt/subcmd/buildinfo" ) type symlinkBuilder struct { @@ -24,17 +24,13 @@ type symlinkBuilder struct { } // TODO: rollback when return err (!= nil) -func (builder *symlinkBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposMap map[pathutil.ReposPath]*buildinfo.Repos) error { +func (builder *symlinkBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposMap map[pathutil.ReposPath]*buildinfo.Repos, lockJSON *lockjson.LockJSON) error { // Exit if vim executable was not found in PATH if _, err := pathutil.VimExecutable(); err != nil { return err } // Get current profile's repos list - lockJSON, err := lockjson.Read() - if err != nil { - return errors.Wrap(err, "could not read lock.json") - } reposList, err := lockJSON.GetCurrentReposList() if err != nil { return err diff --git a/subcmd/buildinfo/buildinfo.go b/gateway/buildinfo/buildinfo.go similarity index 100% rename from subcmd/buildinfo/buildinfo.go rename to gateway/buildinfo/buildinfo.go diff --git a/gateway/cmd.go b/gateway/cmd.go new file mode 100644 index 00000000..2c3c8f7c --- /dev/null +++ b/gateway/cmd.go @@ -0,0 +1,42 @@ +package gateway + +import ( + "flag" + + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/lockjson" +) + +var cmdMap = make(map[string]Cmd) + +// LookUpCmd looks up subcommand by name. +func LookUpCmd(cmd string) Cmd { + return cmdMap[cmd] +} + +// Cmd represents volt's subcommand interface. +// All subcommands must implement this. +type Cmd interface { + ProhibitRootExecution(args []string) bool + Run(cmdctx *CmdContext) *Error + FlagSet() *flag.FlagSet +} + +// CmdContext is a data transfer object between Subcmd and Gateway layer. +type CmdContext struct { + Cmd string + Args []string + LockJSON *lockjson.LockJSON + Config *config.Config +} + +// Error is a command error. +// It also has a exit code. +type Error struct { + Code int + Msg string +} + +func (e *Error) Error() string { + return e.Msg +} diff --git a/subcmd/get.go b/gateway/get.go similarity index 96% rename from subcmd/get.go rename to gateway/get.go index 48ca6c80..7fc3bc84 100644 --- a/subcmd/get.go +++ b/gateway/get.go @@ -1,4 +1,4 @@ -package subcmd +package gateway import ( "flag" @@ -17,12 +17,12 @@ import ( "github.com/vim-volt/volt/config" "github.com/vim-volt/volt/fileutil" + "github.com/vim-volt/volt/gateway/builder" "github.com/vim-volt/volt/gitutil" "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" "github.com/vim-volt/volt/pathutil" "github.com/vim-volt/volt/plugconf" - "github.com/vim-volt/volt/subcmd/builder" "github.com/vim-volt/volt/transaction" multierror "github.com/hashicorp/go-multierror" @@ -116,9 +116,9 @@ Options`) return fs } -func (cmd *getCmd) Run(args []string) *Error { +func (cmd *getCmd) Run(cmdctx *CmdContext) *Error { // Parse args - args, err := cmd.parseArgs(args) + args, err := cmd.parseArgs(cmdctx.Args) if err == ErrShowedHelp { return nil } @@ -126,13 +126,7 @@ func (cmd *getCmd) Run(args []string) *Error { return &Error{Code: 10, Msg: "Failed to parse args: " + err.Error()} } - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return &Error{Code: 11, Msg: "Could not read lock.json: " + err.Error()} - } - - reposPathList, err := cmd.getReposPathList(args, lockJSON) + reposPathList, err := cmd.getReposPathList(args, cmdctx.LockJSON) if err != nil { return &Error{Code: 12, Msg: "Could not get repos list: " + err.Error()} } @@ -140,7 +134,7 @@ func (cmd *getCmd) Run(args []string) *Error { return &Error{Code: 13, Msg: "No repositories are specified"} } - err = cmd.doGet(reposPathList, lockJSON) + err = cmd.doGet(reposPathList, cmdctx.Config, cmdctx.LockJSON) if err != nil { return &Error{Code: 20, Msg: err.Error()} } @@ -187,7 +181,7 @@ func (cmd *getCmd) getReposPathList(args []string, lockJSON *lockjson.LockJSON) return reposPathList, nil } -func (cmd *getCmd) doGet(reposPathList []pathutil.ReposPath, lockJSON *lockjson.LockJSON) error { +func (cmd *getCmd) doGet(reposPathList []pathutil.ReposPath, cfg *config.Config, lockJSON *lockjson.LockJSON) error { // Find matching profile profile, err := lockJSON.Profiles.FindByName(lockJSON.CurrentProfileName) if err != nil { @@ -203,12 +197,6 @@ func (cmd *getCmd) doGet(reposPathList []pathutil.ReposPath, lockJSON *lockjson. } defer transaction.Remove() - // Read config.toml - cfg, err := config.Read() - if err != nil { - return errors.Wrap(err, "could not read config.toml") - } - done := make(chan getParallelResult, len(reposPathList)) getCount := 0 // Invoke installing / upgrading tasks @@ -255,7 +243,7 @@ func (cmd *getCmd) doGet(reposPathList []pathutil.ReposPath, lockJSON *lockjson. } // Build ~/.vim/pack/volt dir - err = builder.Build(false) + err = builder.Build(false, cfg, lockJSON) if err != nil { return errors.Wrap(err, "could not build "+pathutil.VimVoltDir()) } diff --git a/subcmd/get_test.go b/gateway/get_test.go similarity index 99% rename from subcmd/get_test.go rename to gateway/get_test.go index 051f9842..ce391b7d 100644 --- a/subcmd/get_test.go +++ b/gateway/get_test.go @@ -1,4 +1,4 @@ -package subcmd +package gateway import ( "bytes" diff --git a/subcmd/help.go b/gateway/help.go similarity index 95% rename from subcmd/help.go rename to gateway/help.go index 6c94bb45..5d421f29 100644 --- a/subcmd/help.go +++ b/gateway/help.go @@ -1,4 +1,4 @@ -package subcmd +package gateway import ( "flag" @@ -99,7 +99,8 @@ Command return fs } -func (cmd *helpCmd) Run(args []string) *Error { +func (cmd *helpCmd) Run(cmdctx *CmdContext) *Error { + args := cmdctx.Args if len(args) == 0 { cmd.FlagSet().Usage() return nil @@ -112,7 +113,7 @@ func (cmd *helpCmd) Run(args []string) *Error { if !exists { return &Error{Code: 1, Msg: fmt.Sprintf("Unknown command '%s'", args[0])} } - args = append([]string{"-help"}, args[1:]...) - fs.Run(args) + cmdctx.Args = append([]string{"-help"}, args[1:]...) + fs.Run(cmdctx) return nil } diff --git a/subcmd/help_test.go b/gateway/help_test.go similarity index 99% rename from subcmd/help_test.go rename to gateway/help_test.go index 86a5b9af..07c1c667 100644 --- a/subcmd/help_test.go +++ b/gateway/help_test.go @@ -1,4 +1,4 @@ -package subcmd +package gateway import ( "strings" diff --git a/subcmd/list.go b/gateway/list.go similarity index 66% rename from subcmd/list.go rename to gateway/list.go index af734e5e..39ac7dc6 100644 --- a/subcmd/list.go +++ b/gateway/list.go @@ -1,23 +1,27 @@ -package subcmd +package gateway import ( - "encoding/json" "flag" "fmt" - "github.com/pkg/errors" + "io" "os" - "text/template" + "github.com/vim-volt/volt/config" "github.com/vim-volt/volt/lockjson" + "github.com/vim-volt/volt/usecase" ) func init() { - cmdMap["list"] = &listCmd{} + cmdMap["list"] = &listCmd{ + List: usecase.List, + } } type listCmd struct { helped bool format string + + List func(w io.Writer, format string, lockJSON *lockjson.LockJSON, cfg *config.Config) error } func (cmd *listCmd) ProhibitRootExecution(args []string) bool { return false } @@ -125,70 +129,14 @@ repos path: ` } -func (cmd *listCmd) Run(args []string) *Error { +func (cmd *listCmd) Run(cmdctx *CmdContext) *Error { fs := cmd.FlagSet() - fs.Parse(args) + fs.Parse(cmdctx.Args) if cmd.helped { return nil } - if err := cmd.list(cmd.format); err != nil { + if err := cmd.List(os.Stdout, cmd.format, cmdctx.LockJSON, cmdctx.Config); err != nil { return &Error{Code: 10, Msg: "Failed to render template: " + err.Error()} } return nil } - -func (cmd *listCmd) list(format string) error { - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.Wrap(err, "failed to read lock.json") - } - // Parse template string - t, err := template.New("volt").Funcs(cmd.funcMap(lockJSON)).Parse(format) - if err != nil { - return err - } - // Output templated information - return t.Execute(os.Stdout, lockJSON) -} - -func (*listCmd) funcMap(lockJSON *lockjson.LockJSON) template.FuncMap { - profileOf := func(name string) *lockjson.Profile { - profile, err := lockJSON.Profiles.FindByName(name) - if err != nil { - return &lockjson.Profile{} - } - return profile - } - - return template.FuncMap{ - "json": func(value interface{}, args ...string) string { - var b []byte - switch len(args) { - case 0: - b, _ = json.MarshalIndent(value, "", "") - case 1: - b, _ = json.MarshalIndent(value, args[0], "") - default: - b, _ = json.MarshalIndent(value, args[0], args[1]) - } - return string(b) - }, - "currentProfile": func() *lockjson.Profile { - return profileOf(lockJSON.CurrentProfileName) - }, - "profile": profileOf, - "version": func() string { - return voltVersion - }, - "versionMajor": func() int { - return voltVersionInfo()[0] - }, - "versionMinor": func() int { - return voltVersionInfo()[1] - }, - "versionPatch": func() int { - return voltVersionInfo()[2] - }, - } -} diff --git a/subcmd/migrate.go b/gateway/migrate.go similarity index 80% rename from subcmd/migrate.go rename to gateway/migrate.go index 8d22b00d..63d6fb00 100644 --- a/subcmd/migrate.go +++ b/gateway/migrate.go @@ -1,13 +1,14 @@ -package subcmd +package gateway import ( "flag" "fmt" - "github.com/pkg/errors" "os" + "github.com/pkg/errors" + "github.com/vim-volt/volt/logger" - "github.com/vim-volt/volt/subcmd/migrate" + "github.com/vim-volt/volt/usecase" ) func init() { @@ -26,7 +27,7 @@ func (cmd *migrateCmd) FlagSet() *flag.FlagSet { fs.Usage = func() { args := fs.Args() if len(args) > 0 { - m, err := migrate.GetMigrater(args[0]) + m, err := usecase.GetMigrater(args[0]) if err != nil { return } @@ -55,8 +56,8 @@ Available operations`) return fs } -func (cmd *migrateCmd) Run(args []string) *Error { - op, err := cmd.parseArgs(args) +func (cmd *migrateCmd) Run(cmdctx *CmdContext) *Error { + op, err := cmd.parseArgs(cmdctx.Args) if err == ErrShowedHelp { return nil } @@ -64,7 +65,7 @@ func (cmd *migrateCmd) Run(args []string) *Error { return &Error{Code: 10, Msg: "Failed to parse args: " + err.Error()} } - if err := op.Migrate(); err != nil { + if err := op.Migrate(cmdctx.Config, cmdctx.LockJSON); err != nil { return &Error{Code: 11, Msg: "Failed to migrate: " + err.Error()} } @@ -72,7 +73,7 @@ func (cmd *migrateCmd) Run(args []string) *Error { return nil } -func (cmd *migrateCmd) parseArgs(args []string) (migrate.Migrater, error) { +func (cmd *migrateCmd) parseArgs(args []string) (usecase.Migrater, error) { fs := cmd.FlagSet() fs.Parse(args) if cmd.helped { @@ -82,11 +83,11 @@ func (cmd *migrateCmd) parseArgs(args []string) (migrate.Migrater, error) { if len(args) == 0 { return nil, errors.New("please specify migration operation") } - return migrate.GetMigrater(args[0]) + return usecase.GetMigrater(args[0]) } func (cmd *migrateCmd) showAvailableOps(write func(string)) { - for _, m := range migrate.ListMigraters() { + for _, m := range usecase.ListMigraters() { write(fmt.Sprintf(" %s", m.Name())) write(fmt.Sprintf(" %s", m.Description(true))) } diff --git a/subcmd/profile.go b/gateway/profile.go similarity index 80% rename from subcmd/profile.go rename to gateway/profile.go index 6912cb21..93a4589e 100644 --- a/subcmd/profile.go +++ b/gateway/profile.go @@ -1,4 +1,4 @@ -package subcmd +package gateway import ( "flag" @@ -8,11 +8,12 @@ import ( "github.com/pkg/errors" "github.com/hashicorp/go-multierror" + "github.com/vim-volt/volt/gateway/builder" "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" "github.com/vim-volt/volt/pathutil" - "github.com/vim-volt/volt/subcmd/builder" "github.com/vim-volt/volt/transaction" + "github.com/vim-volt/volt/usecase" ) type profileCmd struct { @@ -23,14 +24,16 @@ var profileSubCmd = make(map[string]func([]string) error) func init() { cmdMap["profile"] = &profileCmd{} + cmdMap["enable"] = cmdMap["profile"] + cmdMap["disable"] = cmdMap["profile"] } func (cmd *profileCmd) ProhibitRootExecution(args []string) bool { if len(args) == 0 { return true } - subCmd := args[0] - switch subCmd { + gateway := args[0] + switch gateway { case "show": return false case "list": @@ -68,6 +71,14 @@ Command profile rename {old} {new} Rename profile {old} to {new}. + enable {repository} [{repository2} ...] + This is shortcut of: + volt profile add -current {repository} [{repository2} ...] + + disable {repository} [{repository2} ...] + This is shortcut of: + volt profile rm -current {repository} [{repository2} ...] + profile add [-current | {name}] {repository} [{repository2} ...] Add one or more repositories to profile {name}. @@ -100,9 +111,9 @@ Quick example return fs } -func (cmd *profileCmd) Run(args []string) *Error { +func (cmd *profileCmd) Run(cmdctx *CmdContext) *Error { // Parse args - args, err := cmd.parseArgs(args) + args, err := cmd.parseArgs(cmdctx) if err == ErrShowedHelp { return nil } @@ -111,23 +122,25 @@ func (cmd *profileCmd) Run(args []string) *Error { } subCmd := args[0] + cmdctx.Args = args[1:] + switch subCmd { case "set": - err = cmd.doSet(args[1:]) + err = cmd.doSet(cmdctx) case "show": - err = cmd.doShow(args[1:]) + err = cmd.doShow(cmdctx) case "list": - err = cmd.doList(args[1:]) + err = cmd.doList(cmdctx) case "new": - err = cmd.doNew(args[1:]) + err = cmd.doNew(cmdctx) case "destroy": - err = cmd.doDestroy(args[1:]) + err = cmd.doDestroy(cmdctx) case "rename": - err = cmd.doRename(args[1:]) + err = cmd.doRename(cmdctx) case "add": - err = cmd.doAdd(args[1:]) + err = cmd.doAdd(cmdctx) case "rm": - err = cmd.doRm(args[1:]) + err = cmd.doRm(cmdctx) default: return &Error{Code: 11, Msg: "Unknown subcommand: " + subCmd} } @@ -139,9 +152,18 @@ func (cmd *profileCmd) Run(args []string) *Error { return nil } -func (cmd *profileCmd) parseArgs(args []string) ([]string, error) { +func (cmd *profileCmd) parseArgs(cmdctx *CmdContext) ([]string, error) { + switch cmdctx.Cmd { + case "enable": + cmdctx.Cmd = "profile" + cmdctx.Args = append([]string{"add", "-current"}, cmdctx.Args...) + case "disable": + cmdctx.Cmd = "profile" + cmdctx.Args = append([]string{"rm", "-current"}, cmdctx.Args...) + } + fs := cmd.FlagSet() - fs.Parse(args) + fs.Parse(cmdctx.Args) if cmd.helped { return nil, ErrShowedHelp } @@ -153,15 +175,10 @@ func (cmd *profileCmd) parseArgs(args []string) ([]string, error) { return fs.Args(), nil } -func (*profileCmd) getCurrentProfile() (string, error) { - lockJSON, err := lockjson.Read() - if err != nil { - return "", errors.Wrap(err, "failed to read lock.json") - } - return lockJSON.CurrentProfileName, nil -} +func (cmd *profileCmd) doSet(cmdctx *CmdContext) error { + args := cmdctx.Args + lockJSON := cmdctx.LockJSON -func (cmd *profileCmd) doSet(args []string) error { // Parse args createProfile := false if len(args) > 0 && args[0] == "-n" { @@ -175,27 +192,22 @@ func (cmd *profileCmd) doSet(args []string) error { } profileName := args[0] - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.Wrap(err, "failed to read lock.json") - } - // Exit if current profile is same as profileName if lockJSON.CurrentProfileName == profileName { return errors.Errorf("'%s' is current profile", profileName) } // Create given profile unless the profile exists - if _, err = lockJSON.Profiles.FindByName(profileName); err != nil { + if _, err := lockJSON.Profiles.FindByName(profileName); err != nil { if !createProfile { return err } - if err = cmd.doNew([]string{profileName}); err != nil { + cmdctx.Args = []string{profileName} + if err = cmd.doNew(cmdctx); err != nil { return err } // Read lock.json again - lockJSON, err = lockjson.Read() + err = lockJSON.Reload() if err != nil { return errors.Wrap(err, "failed to read lock.json") } @@ -205,7 +217,7 @@ func (cmd *profileCmd) doSet(args []string) error { } // Begin transaction - err = transaction.Create() + err := transaction.Create() if err != nil { return err } @@ -223,7 +235,7 @@ func (cmd *profileCmd) doSet(args []string) error { logger.Info("Changed current profile: " + profileName) // Build ~/.vim/pack/volt dir - err = builder.Build(false) + err = builder.Build(false, cmdctx.Config, lockJSON) if err != nil { return errors.Wrap(err, "could not build "+pathutil.VimVoltDir()) } @@ -231,19 +243,16 @@ func (cmd *profileCmd) doSet(args []string) error { return nil } -func (cmd *profileCmd) doShow(args []string) error { +func (cmd *profileCmd) doShow(cmdctx *CmdContext) error { + args := cmdctx.Args + lockJSON := cmdctx.LockJSON + if len(args) == 0 { cmd.FlagSet().Usage() logger.Error("'volt profile show' receives profile name.") return nil } - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.Wrap(err, "failed to read lock.json") - } - var profileName string if args[0] == "-current" { profileName = lockJSON.CurrentProfileName @@ -254,25 +263,30 @@ func (cmd *profileCmd) doShow(args []string) error { } } - return (&listCmd{}).list(fmt.Sprintf(`name: %s + format := fmt.Sprintf(`name: %s repos path: {{- with profile %q -}} {{- range .ReposPath }} {{ . }} {{- end -}} {{- end }} -`, profileName, profileName)) +`, profileName, profileName) + return usecase.List(os.Stdout, format, cmdctx.LockJSON, cmdctx.Config) } -func (cmd *profileCmd) doList(args []string) error { - return (&listCmd{}).list(` +func (cmd *profileCmd) doList(cmdctx *CmdContext) error { + format := ` {{- range .Profiles -}} {{- if eq .Name $.CurrentProfileName -}}*{{- else }} {{ end }} {{ .Name }} {{ end -}} -`) +` + return usecase.List(os.Stdout, format, cmdctx.LockJSON, cmdctx.Config) } -func (cmd *profileCmd) doNew(args []string) error { +func (cmd *profileCmd) doNew(cmdctx *CmdContext) error { + args := cmdctx.Args + lockJSON := cmdctx.LockJSON + if len(args) == 0 { cmd.FlagSet().Usage() logger.Error("'volt profile new' receives profile name.") @@ -280,14 +294,8 @@ func (cmd *profileCmd) doNew(args []string) error { } profileName := args[0] - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.Wrap(err, "failed to read lock.json") - } - // Return error if profiles[]/name matches profileName - _, err = lockJSON.Profiles.FindByName(profileName) + _, err := lockJSON.Profiles.FindByName(profileName) if err == nil { return errors.New("profile '" + profileName + "' already exists") } @@ -316,21 +324,18 @@ func (cmd *profileCmd) doNew(args []string) error { return nil } -func (cmd *profileCmd) doDestroy(args []string) error { +func (cmd *profileCmd) doDestroy(cmdctx *CmdContext) error { + args := cmdctx.Args + lockJSON := cmdctx.LockJSON + if len(args) == 0 { cmd.FlagSet().Usage() logger.Error("'volt profile destroy' receives profile name.") return nil } - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.Wrap(err, "failed to read lock.json") - } - // Begin transaction - err = transaction.Create() + err := transaction.Create() if err != nil { return err } @@ -374,7 +379,10 @@ func (cmd *profileCmd) doDestroy(args []string) error { return merr.ErrorOrNil() } -func (cmd *profileCmd) doRename(args []string) error { +func (cmd *profileCmd) doRename(cmdctx *CmdContext) error { + args := cmdctx.Args + lockJSON := cmdctx.LockJSON + if len(args) != 2 { cmd.FlagSet().Usage() logger.Error("'volt profile rename' receives profile name.") @@ -383,12 +391,6 @@ func (cmd *profileCmd) doRename(args []string) error { oldName := args[0] newName := args[1] - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.Wrap(err, "failed to read lock.json") - } - // Return error if profiles[]/name does not match oldName index := lockJSON.Profiles.FindIndexByName(oldName) if index < 0 { @@ -401,7 +403,7 @@ func (cmd *profileCmd) doRename(args []string) error { } // Begin transaction - err = transaction.Create() + err := transaction.Create() if err != nil { return err } @@ -433,12 +435,9 @@ func (cmd *profileCmd) doRename(args []string) error { return nil } -func (cmd *profileCmd) doAdd(args []string) error { - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.Wrap(err, "failed to read lock.json") - } +func (cmd *profileCmd) doAdd(cmdctx *CmdContext) error { + args := cmdctx.Args + lockJSON := cmdctx.LockJSON // Parse args profileName, reposPathList, err := cmd.parseAddArgs(lockJSON, "add", args) @@ -467,7 +466,7 @@ func (cmd *profileCmd) doAdd(args []string) error { } // Build ~/.vim/pack/volt dir - err = builder.Build(false) + err = builder.Build(false, cmdctx.Config, lockJSON) if err != nil { return errors.Wrap(err, "could not build "+pathutil.VimVoltDir()) } @@ -475,12 +474,9 @@ func (cmd *profileCmd) doAdd(args []string) error { return nil } -func (cmd *profileCmd) doRm(args []string) error { - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.Wrap(err, "failed to read lock.json") - } +func (cmd *profileCmd) doRm(cmdctx *CmdContext) error { + args := cmdctx.Args + lockJSON := cmdctx.LockJSON // Parse args profileName, reposPathList, err := cmd.parseAddArgs(lockJSON, "rm", args) @@ -511,7 +507,7 @@ func (cmd *profileCmd) doRm(args []string) error { } // Build ~/.vim/pack/volt dir - err = builder.Build(false) + err = builder.Build(false, cmdctx.Config, lockJSON) if err != nil { return errors.Wrap(err, "could not build "+pathutil.VimVoltDir()) } diff --git a/subcmd/profile_test.go b/gateway/profile_test.go similarity index 99% rename from subcmd/profile_test.go rename to gateway/profile_test.go index feea4621..44612aad 100644 --- a/subcmd/profile_test.go +++ b/gateway/profile_test.go @@ -1,4 +1,4 @@ -package subcmd +package gateway import ( "encoding/json" diff --git a/subcmd/rm.go b/gateway/rm.go similarity index 92% rename from subcmd/rm.go rename to gateway/rm.go index b2e4d309..143502a3 100644 --- a/subcmd/rm.go +++ b/gateway/rm.go @@ -1,4 +1,4 @@ -package subcmd +package gateway import ( "flag" @@ -10,11 +10,11 @@ import ( "github.com/pkg/errors" "github.com/vim-volt/volt/fileutil" + "github.com/vim-volt/volt/gateway/builder" "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" "github.com/vim-volt/volt/pathutil" "github.com/vim-volt/volt/plugconf" - "github.com/vim-volt/volt/subcmd/builder" "github.com/vim-volt/volt/transaction" ) @@ -63,8 +63,8 @@ Description return fs } -func (cmd *rmCmd) Run(args []string) *Error { - reposPathList, err := cmd.parseArgs(args) +func (cmd *rmCmd) Run(cmdctx *CmdContext) *Error { + reposPathList, err := cmd.parseArgs(cmdctx.Args) if err == ErrShowedHelp { return nil } @@ -72,13 +72,13 @@ func (cmd *rmCmd) Run(args []string) *Error { return &Error{Code: 10, Msg: err.Error()} } - err = cmd.doRemove(reposPathList) + err = cmd.doRemove(reposPathList, cmdctx.LockJSON) if err != nil { return &Error{Code: 11, Msg: "Failed to remove repository: " + err.Error()} } // Build opt dir - err = builder.Build(false) + err = builder.Build(false, cmdctx.Config, cmdctx.LockJSON) if err != nil { return &Error{Code: 12, Msg: "Could not build " + pathutil.VimVoltDir() + ": " + err.Error()} } @@ -109,15 +109,9 @@ func (cmd *rmCmd) parseArgs(args []string) ([]pathutil.ReposPath, error) { return reposPathList, nil } -func (cmd *rmCmd) doRemove(reposPathList []pathutil.ReposPath) error { - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return err - } - +func (cmd *rmCmd) doRemove(reposPathList []pathutil.ReposPath, lockJSON *lockjson.LockJSON) error { // Begin transaction - err = transaction.Create() + err := transaction.Create() if err != nil { return err } diff --git a/subcmd/rm_test.go b/gateway/rm_test.go similarity index 99% rename from subcmd/rm_test.go rename to gateway/rm_test.go index 46616974..5092f183 100644 --- a/subcmd/rm_test.go +++ b/gateway/rm_test.go @@ -1,4 +1,4 @@ -package subcmd +package gateway import ( "fmt" diff --git a/gateway/self_upgrade.go b/gateway/self_upgrade.go new file mode 100644 index 00000000..d2e9ae12 --- /dev/null +++ b/gateway/self_upgrade.go @@ -0,0 +1,82 @@ +package gateway + +import ( + "flag" + "fmt" + "os" + "strconv" + + "github.com/vim-volt/volt/usecase" +) + +func init() { + cmdMap["self-upgrade"] = &selfUpgradeCmd{ + SelfUpgrade: usecase.SelfUpgrade, + RemoveOldBinary: usecase.RemoveOldBinary, + } +} + +type selfUpgradeCmd struct { + helped bool + checkOnly bool + + SelfUpgrade func(latestURL string, checkOnly bool) error + RemoveOldBinary func(ppid int) error +} + +func (cmd *selfUpgradeCmd) ProhibitRootExecution(args []string) bool { return true } + +func (cmd *selfUpgradeCmd) FlagSet() *flag.FlagSet { + fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + fs.SetOutput(os.Stdout) + fs.Usage = func() { + fmt.Print(` +Usage + volt self-upgrade [-help] [-check] + +Description + Upgrade to the latest volt command, or if -check was given, it only checks the newer version is available.` + "\n\n") + //fmt.Println("Options") + //fs.PrintDefaults() + fmt.Println() + cmd.helped = true + } + fs.BoolVar(&cmd.checkOnly, "check", false, "only checks the newer version is available") + return fs +} + +func (cmd *selfUpgradeCmd) Run(cmdctx *CmdContext) *Error { + err := cmd.parseArgs(cmdctx.Args) + if err == ErrShowedHelp { + return nil + } + if err != nil { + return &Error{Code: 10, Msg: "Failed to parse args: " + err.Error()} + } + + if ppidStr := os.Getenv("VOLT_SELF_UPGRADE_PPID"); ppidStr != "" { + ppid, err := strconv.Atoi(ppidStr) + if err != nil { + return &Error{Code: 20, Msg: "Failed to parse VOLT_SELF_UPGRADE_PPID: " + err.Error()} + } + if err = cmd.RemoveOldBinary(ppid); err != nil { + return &Error{Code: 11, Msg: "Failed to clean up old binary: " + err.Error()} + } + } else { + latestURL := "https://api.github.com/repos/vim-volt/volt/releases/latest" + if err = cmd.SelfUpgrade(latestURL, cmd.checkOnly); err != nil { + return &Error{Code: 12, Msg: "Failed to self-upgrade: " + err.Error()} + } + } + + return nil +} + +func (cmd *selfUpgradeCmd) parseArgs(args []string) error { + fs := cmd.FlagSet() + fs.Parse(args) + if cmd.helped { + return ErrShowedHelp + } + return nil +} diff --git a/gateway/version.go b/gateway/version.go new file mode 100644 index 00000000..06522484 --- /dev/null +++ b/gateway/version.go @@ -0,0 +1,50 @@ +package gateway + +import ( + "flag" + "fmt" + "os" + + "github.com/vim-volt/volt/usecase" +) + +func init() { + cmdMap["version"] = &versionCmd{VersionString: usecase.VersionString()} +} + +type versionCmd struct { + helped bool + + VersionString string +} + +func (cmd *versionCmd) ProhibitRootExecution(args []string) bool { return false } + +func (cmd *versionCmd) FlagSet() *flag.FlagSet { + fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + fs.SetOutput(os.Stdout) + fs.Usage = func() { + fmt.Print(` +Usage + volt version [-help] + +Description + Show current version of volt.` + "\n\n") + //fmt.Println("Options") + //fs.PrintDefaults() + fmt.Println() + cmd.helped = true + } + return fs +} + +func (cmd *versionCmd) Run(cmdctx *CmdContext) *Error { + fs := cmd.FlagSet() + fs.Parse(cmdctx.Args) + if cmd.helped { + return nil + } + + fmt.Printf("volt version: %s\n", cmd.VersionString) + return nil +} diff --git a/lockjson/lockjson.go b/lockjson/lockjson.go index cf3dab90..3646dc3e 100644 --- a/lockjson/lockjson.go +++ b/lockjson/lockjson.go @@ -56,46 +56,48 @@ type Profile struct { const lockJSONVersion = 2 -func initialLockJSON() *LockJSON { - return &LockJSON{ - Version: lockJSONVersion, - CurrentProfileName: "default", - Repos: make([]Repos, 0), - Profiles: []Profile{ - Profile{ - Name: "default", - ReposPath: make([]pathutil.ReposPath, 0), - }, +func initLockJSON(lockJSON *LockJSON) { + lockJSON.Version = lockJSONVersion + lockJSON.CurrentProfileName = "default" + lockJSON.Repos = make([]Repos, 0) + lockJSON.Profiles = []Profile{ + Profile{ + Name: "default", + ReposPath: make([]pathutil.ReposPath, 0), }, } } // Read reads from lock.json and returns LockJSON func Read() (*LockJSON, error) { - return read(true) + var lockJSON LockJSON + err := read(true, &lockJSON) + return &lockJSON, err } // ReadNoMigrationMsg is same as Read, but no migration message is printed. func ReadNoMigrationMsg() (*LockJSON, error) { - return read(false) + var lockJSON LockJSON + err := read(false, &lockJSON) + return &lockJSON, err } -func read(doLog bool) (*LockJSON, error) { +func read(doLog bool, lockJSON *LockJSON) error { // Return initial lock.json struct if lockfile does not exist lockfile := pathutil.LockJSON() if !pathutil.Exists(lockfile) { - return initialLockJSON(), nil + initLockJSON(lockJSON) + return nil } // Read lock.json bytes, err := ioutil.ReadFile(lockfile) if err != nil { - return nil, err + return err } - var lockJSON LockJSON - err = json.Unmarshal(bytes, &lockJSON) + err = json.Unmarshal(bytes, lockJSON) if err != nil { - return nil, err + return err } if lockJSON.Version < lockJSONVersion { @@ -103,19 +105,18 @@ func read(doLog bool) (*LockJSON, error) { logger.Warnf("Performing auto-migration of lock.json: v%d -> v%d", lockJSON.Version, lockJSONVersion) logger.Warn("Please run 'volt migrate' to migrate explicitly if it's not updated by after operations") } - err = migrate(bytes, &lockJSON) + err = migrate(bytes, lockJSON) if err != nil { - return nil, err + return err } } // Validate lock.json - err = validate(&lockJSON) + err = validate(lockJSON) if err != nil { - return nil, errors.Wrap(err, "validation failed: lock.json") + return errors.Wrap(err, "validation failed: lock.json") } - - return &lockJSON, nil + return nil } func validate(lockJSON *LockJSON) error { @@ -252,6 +253,11 @@ func validateMissing(lockJSON *LockJSON) error { return nil } +// Reload reads lock.json again from filesystem. +func (lockJSON *LockJSON) Reload() error { + return read(true, lockJSON) +} + func (lockJSON *LockJSON) Write() error { // Validate lock.json err := validate(lockJSON) diff --git a/main.go b/main.go index 70334e02..05ee6c19 100644 --- a/main.go +++ b/main.go @@ -4,15 +4,104 @@ package main import ( "os" + "os/user" + "runtime" + "github.com/pkg/errors" + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/gateway" + "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" - "github.com/vim-volt/volt/subcmd" ) func main() { - err := subcmd.Run(os.Args, subcmd.DefaultRunner) + code, msg := run(os.Args) + if code != 0 { + logger.Error(msg) + os.Exit(code) + } +} + +func run(args []string) (int, string) { + if os.Getenv("VOLT_DEBUG") != "" { + logger.SetLevel(logger.DebugLevel) + } + + if len(args) <= 1 { + args = append(args, "help") + } + subCmd := args[1] + args = args[2:] + + // Expand subcommand alias + subCmd, args, err := expandAlias(subCmd, args) + if err != nil { + return 1, err.Error() + } + + c := gateway.LookUpCmd(subCmd) + if c == nil { + return 3, "Unknown command '" + subCmd + "'" + } + + // Disallow executing the commands which may modify files in root priviledge + if c.ProhibitRootExecution(args) { + err := detectPriviledgedUser() + if err != nil { + return 4, err.Error() + } + } + + lockJSON, err := lockjson.Read() + if err != nil { + return 20, errors.Wrap(err, "failed to read lock.json").Error() + } + + cfg, err := config.Read() + if err != nil { + return 30, errors.Wrap(err, "failed to read config.toml").Error() + } + + result := c.Run(&gateway.CmdContext{ + Cmd: subCmd, + Args: args, + LockJSON: lockJSON, + Config: cfg, + }) + if result != nil { + return result.Code, result.Msg + } + return 0, "" +} + +func expandAlias(subCmd string, args []string) (string, []string, error) { + cfg, err := config.Read() if err != nil { - logger.Error(err.Msg) - os.Exit(err.Code) + return "", nil, errors.Wrap(err, "could not read config.toml") + } + if newArgs, exists := cfg.Alias[subCmd]; exists && len(newArgs) > 0 { + subCmd = newArgs[0] + args = append(newArgs[1:], args...) + } + return subCmd, args, nil +} + +// On Windows, this function always returns nil. +// Because if even administrator user creates a file, the file can be +// overwritten by normal user. +// On Linux, if current user's uid == 0, returns non-nil error. +func detectPriviledgedUser() error { + if runtime.GOOS == "windows" { + return nil + } + u, err := user.Current() + if err != nil { + return errors.Wrap(err, "cannot get current user") + } + if u.Uid == "0" { + return errors.New( + "cannot run this sub command with root priviledge. " + + "Please run as normal user") } + return nil } diff --git a/subcmd/cmd.go b/subcmd/cmd.go deleted file mode 100644 index 67a88f74..00000000 --- a/subcmd/cmd.go +++ /dev/null @@ -1,108 +0,0 @@ -package subcmd - -import ( - "flag" - "github.com/pkg/errors" - "os" - "os/user" - "runtime" - - "github.com/vim-volt/volt/config" - "github.com/vim-volt/volt/logger" -) - -var cmdMap = make(map[string]Cmd) - -// Cmd represents volt's subcommand interface. -// All subcommands must implement this. -type Cmd interface { - ProhibitRootExecution(args []string) bool - Run(args []string) *Error - FlagSet() *flag.FlagSet -} - -// RunnerFunc invokes c with args. -// On unit testing, a mock function was given. -type RunnerFunc func(c Cmd, args []string) *Error - -// Error is a command error. -// It also has a exit code. -type Error struct { - Code int - Msg string -} - -func (e *Error) Error() string { - return e.Msg -} - -// DefaultRunner simply runs command with args -func DefaultRunner(c Cmd, args []string) *Error { - return c.Run(args) -} - -// Run is invoked by main(), each argument means 'volt {subcmd} {args}'. -func Run(args []string, cont RunnerFunc) *Error { - if os.Getenv("VOLT_DEBUG") != "" { - logger.SetLevel(logger.DebugLevel) - } - - if len(args) <= 1 { - args = append(args, "help") - } - subCmd := args[1] - args = args[2:] - - // Expand subcommand alias - subCmd, args, err := expandAlias(subCmd, args) - if err != nil { - return &Error{Code: 1, Msg: err.Error()} - } - - c, exists := cmdMap[subCmd] - if !exists { - return &Error{Code: 3, Msg: "unknown command '" + subCmd + "'"} - } - - // Disallow executing the commands which may modify files in root priviledge - if c.ProhibitRootExecution(args) { - err := detectPriviledgedUser() - if err != nil { - return &Error{Code: 4, Msg: err.Error()} - } - } - - return cont(c, args) -} - -func expandAlias(subCmd string, args []string) (string, []string, error) { - cfg, err := config.Read() - if err != nil { - return "", nil, errors.Wrap(err, "could not read config.toml") - } - if newArgs, exists := cfg.Alias[subCmd]; exists && len(newArgs) > 0 { - subCmd = newArgs[0] - args = append(newArgs[1:], args...) - } - return subCmd, args, nil -} - -// On Windows, this function always returns nil. -// Because if even administrator user creates a file, the file can be -// overwritten by normal user. -// On Linux, if current user's uid == 0, returns non-nil error. -func detectPriviledgedUser() error { - if runtime.GOOS == "windows" { - return nil - } - u, err := user.Current() - if err != nil { - return errors.Wrap(err, "cannot get current user") - } - if u.Uid == "0" { - return errors.New( - "cannot run this sub command with root priviledge. " + - "Please run as normal user") - } - return nil -} diff --git a/subcmd/disable.go b/subcmd/disable.go deleted file mode 100644 index 8ed15114..00000000 --- a/subcmd/disable.go +++ /dev/null @@ -1,88 +0,0 @@ -package subcmd - -import ( - "flag" - "fmt" - "github.com/pkg/errors" - "os" - - "github.com/vim-volt/volt/pathutil" -) - -func init() { - cmdMap["disable"] = &disableCmd{} -} - -type disableCmd struct { - helped bool -} - -func (cmd *disableCmd) ProhibitRootExecution(args []string) bool { return true } - -func (cmd *disableCmd) FlagSet() *flag.FlagSet { - fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) - fs.SetOutput(os.Stdout) - fs.Usage = func() { - fmt.Print(` -Usage - volt disable [-help] {repository} [{repository2} ...] - -Quick example - $ volt disable tyru/caw.vim # will disable tyru/caw.vim plugin in current profile - -Description - This is shortcut of: - volt profile rm {current profile} {repository} [{repository2} ...]` + "\n\n") - //fmt.Println("Options") - //fs.PrintDefaults() - fmt.Println() - cmd.helped = true - } - return fs -} - -func (cmd *disableCmd) Run(args []string) *Error { - reposPathList, err := cmd.parseArgs(args) - if err == ErrShowedHelp { - return nil - } - if err != nil { - return &Error{Code: 10, Msg: "Failed to parse args: " + err.Error()} - } - - profCmd := profileCmd{} - err = profCmd.doRm(append( - []string{"-current"}, - reposPathList.Strings()..., - )) - if err != nil { - return &Error{Code: 11, Msg: err.Error()} - } - - return nil -} - -func (cmd *disableCmd) parseArgs(args []string) (pathutil.ReposPathList, error) { - fs := cmd.FlagSet() - fs.Parse(args) - if cmd.helped { - return nil, ErrShowedHelp - } - - if len(fs.Args()) == 0 { - fs.Usage() - return nil, errors.New("repository was not given") - } - - // Normalize repos path - reposPathList := make(pathutil.ReposPathList, 0, len(fs.Args())) - for _, arg := range fs.Args() { - reposPath, err := pathutil.NormalizeRepos(arg) - if err != nil { - return nil, err - } - reposPathList = append(reposPathList, reposPath) - } - - return reposPathList, nil -} diff --git a/subcmd/enable.go b/subcmd/enable.go deleted file mode 100644 index bea7586a..00000000 --- a/subcmd/enable.go +++ /dev/null @@ -1,88 +0,0 @@ -package subcmd - -import ( - "flag" - "fmt" - "github.com/pkg/errors" - "os" - - "github.com/vim-volt/volt/pathutil" -) - -func init() { - cmdMap["enable"] = &enableCmd{} -} - -type enableCmd struct { - helped bool -} - -func (cmd *enableCmd) ProhibitRootExecution(args []string) bool { return true } - -func (cmd *enableCmd) FlagSet() *flag.FlagSet { - fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) - fs.SetOutput(os.Stdout) - fs.Usage = func() { - fmt.Print(` -Usage - volt enable [-help] {repository} [{repository2} ...] - -Quick example - $ volt enable tyru/caw.vim # will enable tyru/caw.vim plugin in current profile - -Description - This is shortcut of: - volt profile add {current profile} {repository} [{repository2} ...]` + "\n\n") - //fmt.Println("Options") - //fs.PrintDefaults() - fmt.Println() - cmd.helped = true - } - return fs -} - -func (cmd *enableCmd) Run(args []string) *Error { - reposPathList, err := cmd.parseArgs(args) - if err == ErrShowedHelp { - return nil - } - if err != nil { - return &Error{Code: 10, Msg: "Failed to parse args: " + err.Error()} - } - - profCmd := profileCmd{} - err = profCmd.doAdd(append( - []string{"-current"}, - reposPathList.Strings()..., - )) - if err != nil { - return &Error{Code: 11, Msg: err.Error()} - } - - return nil -} - -func (cmd *enableCmd) parseArgs(args []string) (pathutil.ReposPathList, error) { - fs := cmd.FlagSet() - fs.Parse(args) - if cmd.helped { - return nil, ErrShowedHelp - } - - if len(fs.Args()) == 0 { - fs.Usage() - return nil, errors.New("repository was not given") - } - - // Normalize repos path - reposPathList := make(pathutil.ReposPathList, 0, len(fs.Args())) - for _, arg := range fs.Args() { - reposPath, err := pathutil.NormalizeRepos(arg) - if err != nil { - return nil, err - } - reposPathList = append(reposPathList, reposPath) - } - - return reposPathList, nil -} diff --git a/subcmd/self_upgrade.go b/subcmd/self_upgrade.go deleted file mode 100644 index 00d86445..00000000 --- a/subcmd/self_upgrade.go +++ /dev/null @@ -1,232 +0,0 @@ -package subcmd - -import ( - "encoding/json" - "flag" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "syscall" - "time" - - "github.com/pkg/errors" - - "github.com/vim-volt/volt/httputil" - "github.com/vim-volt/volt/logger" -) - -func init() { - cmdMap["self-upgrade"] = &selfUpgradeCmd{} -} - -type selfUpgradeCmd struct { - helped bool - check bool -} - -func (cmd *selfUpgradeCmd) ProhibitRootExecution(args []string) bool { return true } - -func (cmd *selfUpgradeCmd) FlagSet() *flag.FlagSet { - fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) - fs.SetOutput(os.Stdout) - fs.Usage = func() { - fmt.Print(` -Usage - volt self-upgrade [-help] [-check] - -Description - Upgrade to the latest volt command, or if -check was given, it only checks the newer version is available.` + "\n\n") - //fmt.Println("Options") - //fs.PrintDefaults() - fmt.Println() - cmd.helped = true - } - fs.BoolVar(&cmd.check, "check", false, "only checks the newer version is available") - return fs -} - -func (cmd *selfUpgradeCmd) Run(args []string) *Error { - err := cmd.parseArgs(args) - if err == ErrShowedHelp { - return nil - } - if err != nil { - return &Error{Code: 10, Msg: "Failed to parse args: " + err.Error()} - } - - if ppidStr := os.Getenv("VOLT_SELF_UPGRADE_PPID"); ppidStr != "" { - if err = cmd.doCleanUp(ppidStr); err != nil { - return &Error{Code: 11, Msg: "Failed to clean up old binary: " + err.Error()} - } - } else { - latestURL := "https://api.github.com/repos/vim-volt/volt/releases/latest" - if err = cmd.doSelfUpgrade(latestURL); err != nil { - return &Error{Code: 12, Msg: "Failed to self-upgrade: " + err.Error()} - } - } - - return nil -} - -func (cmd *selfUpgradeCmd) parseArgs(args []string) error { - fs := cmd.FlagSet() - fs.Parse(args) - if cmd.helped { - return ErrShowedHelp - } - return nil -} - -func (cmd *selfUpgradeCmd) doCleanUp(ppidStr string) error { - ppid, err := strconv.Atoi(ppidStr) - if err != nil { - return errors.Wrap(err, "failed to parse VOLT_SELF_UPGRADE_PPID") - } - - // Wait until the parent process exits - if died := cmd.waitUntilParentExits(ppid); !died { - return errors.Errorf("parent pid (%s) is keeping alive for long time", ppidStr) - } - - // Remove old binary - voltExe, err := cmd.getExecutablePath() - if err != nil { - return err - } - return os.Remove(voltExe + ".old") -} - -func (cmd *selfUpgradeCmd) waitUntilParentExits(pid int) bool { - fib := []int{1, 1, 2, 3, 5, 8, 13} // 33 second - for i := 0; i < len(fib); i++ { - if !cmd.processIsAlive(pid) { - return true - } - time.Sleep(time.Duration(fib[i]) * time.Second) - } - return false -} - -func (*selfUpgradeCmd) processIsAlive(pid int) bool { - process, err := os.FindProcess(pid) - if err != nil { - return false - } - err = process.Signal(syscall.Signal(0)) - return err == nil -} - -type latestRelease struct { - TagName string `json:"tag_name"` - Body string `json:"body"` - Assets []releaseAsset -} - -type releaseAsset struct { - BrowserDownloadURL string `json:"browser_download_url"` - Name string `json:"name"` -} - -func (cmd *selfUpgradeCmd) doSelfUpgrade(latestURL string) error { - // Check the latest binary info - release, err := cmd.checkLatest(latestURL) - if err != nil { - return err - } - logger.Debugf("tag_name = %q", release.TagName) - tagNameVer, err := parseVersion(release.TagName) - if err != nil { - return err - } - if compareVersion(tagNameVer, voltVersionInfo()) <= 0 { - logger.Info("No updates were found.") - return nil - } - logger.Infof("Found update: %s -> %s", voltVersion, release.TagName) - - // Show release note - fmt.Println("---") - fmt.Println(release.Body) - fmt.Println("---") - - if cmd.check { - return nil - } - - // Download the latest binary as "volt[.exe].latest" - voltExe, err := cmd.getExecutablePath() - if err != nil { - return err - } - latestFile, err := os.OpenFile(voltExe+".latest", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) - if err != nil { - return err - } - err = cmd.download(latestFile, release) - latestFile.Close() - if err != nil { - return err - } - - // Rename dir/volt[.exe] to dir/volt[.exe].old - // NOTE: Windows can rename running executable file - if err := os.Rename(voltExe, voltExe+".old"); err != nil { - return err - } - - // Rename dir/volt[.exe].latest to dir/volt[.exe] - if err := os.Rename(voltExe+".latest", voltExe); err != nil { - return err - } - - // Spawn dir/volt[.exe] with env "VOLT_SELF_UPGRADE_PPID={pid}" - voltCmd := exec.Command(voltExe, "self-upgrade") - if err = voltCmd.Start(); err != nil { - return err - } - return nil -} - -func (*selfUpgradeCmd) getExecutablePath() (string, error) { - exe, err := os.Executable() - if err != nil { - return "", err - } - return filepath.EvalSymlinks(exe) -} - -func (*selfUpgradeCmd) checkLatest(url string) (*latestRelease, error) { - content, err := httputil.GetContent(url) - if err != nil { - return nil, err - } - var release latestRelease - if err = json.Unmarshal(content, &release); err != nil { - return nil, err - } - return &release, nil -} - -func (*selfUpgradeCmd) download(w io.Writer, release *latestRelease) error { - suffix := runtime.GOOS + "-" + runtime.GOARCH - for i := range release.Assets { - // e.g.: Name = "volt-v0.1.2-linux-amd64" - if strings.HasSuffix(release.Assets[i].Name, suffix) { - r, err := httputil.GetContentReader(release.Assets[i].BrowserDownloadURL) - if err != nil { - return err - } - defer r.Close() - if _, err = io.Copy(w, r); err != nil { - return err - } - break - } - } - return nil -} diff --git a/subcmd/version.go b/subcmd/version.go deleted file mode 100644 index 2d8cde46..00000000 --- a/subcmd/version.go +++ /dev/null @@ -1,110 +0,0 @@ -package subcmd - -import ( - "flag" - "fmt" - "github.com/pkg/errors" - "os" - "regexp" - "strconv" -) - -// This variable is not constant for testing (to change it temporarily) -var voltVersion = "v0.3.6-alpha" - -func init() { - cmdMap["version"] = &versionCmd{} -} - -type versionCmd struct { - helped bool -} - -func (cmd *versionCmd) ProhibitRootExecution(args []string) bool { return false } - -func (cmd *versionCmd) FlagSet() *flag.FlagSet { - fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) - fs.SetOutput(os.Stdout) - fs.Usage = func() { - fmt.Print(` -Usage - volt version [-help] - -Description - Show current version of volt.` + "\n\n") - //fmt.Println("Options") - //fs.PrintDefaults() - fmt.Println() - cmd.helped = true - } - return fs -} - -func (cmd *versionCmd) Run(args []string) *Error { - fs := cmd.FlagSet() - fs.Parse(args) - if cmd.helped { - return nil - } - - fmt.Printf("volt version: %s\n", voltVersion) - return nil -} - -// [major, minor, patch, alphaBeta] -type versionInfo []int - -const ( - suffixAlpha = 1 - suffixBeta = 2 - suffixStable = 9 -) - -var rxVersion = regexp.MustCompile(`^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(-alpha|-beta)?`) - -func voltVersionInfo() versionInfo { - // parseVersion(voltVersionInfo) must not return non-nil error! - voltVersionInfo, err := parseVersion(voltVersion) - if err != nil { - panic(err) - } - return voltVersionInfo -} - -func compareVersion(v1, v2 versionInfo) int { - for i := 0; i < 4; i++ { - if v1[i] > v2[i] { - return 1 - } else if v1[i] < v2[i] { - return -1 - } - } - return 0 -} - -func parseVersion(ver string) (versionInfo, error) { - m := rxVersion.FindStringSubmatch(ver) - if len(m) == 0 { - return nil, errors.New("version number format is invalid: " + ver) - } - info := make(versionInfo, 0, 4) - for i := 1; i <= 3 && m[i] != ""; i++ { - n, err := strconv.Atoi(m[i]) - if err != nil { - return nil, err - } - info = append(info, n) - } - if len(info) == 2 { - info = append(info, 0) - } - switch m[4] { - case "": - info = append(info, suffixStable) - case "-alpha": - info = append(info, suffixAlpha) - case "-beta": - info = append(info, suffixBeta) - } - return info, nil -} diff --git a/usecase/list.go b/usecase/list.go new file mode 100644 index 00000000..a84a7e94 --- /dev/null +++ b/usecase/list.go @@ -0,0 +1,62 @@ +package usecase + +import ( + "encoding/json" + "html/template" + "io" + + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/lockjson" +) + +// List renders text/template format format to w with paramter lockJSON, cfg. +func List(w io.Writer, format string, lockJSON *lockjson.LockJSON, cfg *config.Config) error { + // Parse template string + t, err := template.New("volt").Funcs(funcMap(lockJSON)).Parse(format) + if err != nil { + return err + } + // Output templated information + return t.Execute(w, lockJSON) +} + +func funcMap(lockJSON *lockjson.LockJSON) template.FuncMap { + profileOf := func(name string) *lockjson.Profile { + profile, err := lockJSON.Profiles.FindByName(name) + if err != nil { + return &lockjson.Profile{} + } + return profile + } + + return template.FuncMap{ + "json": func(value interface{}, args ...string) string { + var b []byte + switch len(args) { + case 0: + b, _ = json.MarshalIndent(value, "", "") + case 1: + b, _ = json.MarshalIndent(value, args[0], "") + default: + b, _ = json.MarshalIndent(value, args[0], args[1]) + } + return string(b) + }, + "currentProfile": func() *lockjson.Profile { + return profileOf(lockJSON.CurrentProfileName) + }, + "profile": profileOf, + "version": func() string { + return VersionString() + }, + "versionMajor": func() int { + return Version()[0] + }, + "versionMinor": func() int { + return Version()[1] + }, + "versionPatch": func() int { + return Version()[2] + }, + } +} diff --git a/subcmd/list_test.go b/usecase/list_test.go similarity index 93% rename from subcmd/list_test.go rename to usecase/list_test.go index 0f2d0f03..9695b86d 100644 --- a/subcmd/list_test.go +++ b/usecase/list_test.go @@ -1,4 +1,4 @@ -package subcmd +package usecase import ( "strconv" @@ -106,8 +106,8 @@ func TestVoltListFunctions(t *testing.T) { testutil.SuccessExit(t, out, err) // (b) - if string(out) != voltVersion { - t.Errorf("expected %q but got %q", voltVersion, string(out)) + if string(out) != VoltVersion { + t.Errorf("expected %q but got %q", VoltVersion, string(out)) } }) @@ -123,7 +123,7 @@ func TestVoltListFunctions(t *testing.T) { testutil.SuccessExit(t, out, err) // (c) - expected := strconv.Itoa(voltVersionInfo()[0]) + expected := strconv.Itoa(Version()[0]) if string(out) != expected { t.Errorf("expected %q but got %q", expected, string(out)) } @@ -141,7 +141,7 @@ func TestVoltListFunctions(t *testing.T) { testutil.SuccessExit(t, out, err) // (d) - expected := strconv.Itoa(voltVersionInfo()[1]) + expected := strconv.Itoa(Version()[1]) if string(out) != expected { t.Errorf("expected %q but got %q", expected, string(out)) } @@ -159,7 +159,7 @@ func TestVoltListFunctions(t *testing.T) { testutil.SuccessExit(t, out, err) // (e) - expected := strconv.Itoa(voltVersionInfo()[2]) + expected := strconv.Itoa(Version()[2]) if string(out) != expected { t.Errorf("expected %q but got %q", expected, string(out)) } diff --git a/subcmd/migrate/lockjson.go b/usecase/migrate-lockjson.go similarity index 85% rename from subcmd/migrate/lockjson.go rename to usecase/migrate-lockjson.go index fb3e963b..b45ff5b0 100644 --- a/subcmd/migrate/lockjson.go +++ b/usecase/migrate-lockjson.go @@ -1,8 +1,9 @@ -package migrate +package usecase import ( "github.com/pkg/errors" + "github.com/vim-volt/volt/config" "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/transaction" ) @@ -31,15 +32,9 @@ Description To suppress this, running this command simply reads and writes migrated structure to lock.json.` } -func (*lockjsonMigrater) Migrate() error { - // Read lock.json - lockJSON, err := lockjson.ReadNoMigrationMsg() - if err != nil { - return errors.Wrap(err, "could not read lock.json") - } - +func (*lockjsonMigrater) Migrate(cfg *config.Config, lockJSON *lockjson.LockJSON) error { // Begin transaction - err = transaction.Create() + err := transaction.Create() if err != nil { return err } diff --git a/subcmd/migrate/plugconf-config-func.go b/usecase/migrate-plugconf-config-func.go similarity index 86% rename from subcmd/migrate/plugconf-config-func.go rename to usecase/migrate-plugconf-config-func.go index 73d44ab8..a5ed8a6b 100644 --- a/subcmd/migrate/plugconf-config-func.go +++ b/usecase/migrate-plugconf-config-func.go @@ -1,17 +1,19 @@ -package migrate +package usecase import ( - "github.com/pkg/errors" "io/ioutil" "os" "path/filepath" "strings" + "github.com/pkg/errors" + + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/gateway/builder" "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" "github.com/vim-volt/volt/pathutil" "github.com/vim-volt/volt/plugconf" - "github.com/vim-volt/volt/subcmd/builder" "github.com/vim-volt/volt/transaction" ) @@ -39,13 +41,7 @@ Description All plugconf files are replaced with new contents.` } -func (*plugconfConfigMigrater) Migrate() error { - // Read lock.json - lockJSON, err := lockjson.ReadNoMigrationMsg() - if err != nil { - return errors.Wrap(err, "could not read lock.json") - } - +func (*plugconfConfigMigrater) Migrate(cfg *config.Config, lockJSON *lockjson.LockJSON) error { results, parseErr := plugconf.ParseMultiPlugconf(lockJSON.Repos) if parseErr.HasErrs() { logger.Error("Please fix the following errors before migration:") @@ -82,21 +78,21 @@ func (*plugconfConfigMigrater) Migrate() error { // After checking errors, write the content to files for _, info := range infoList { os.MkdirAll(filepath.Dir(info.path), 0755) - err = ioutil.WriteFile(info.path, info.content, 0644) + err := ioutil.WriteFile(info.path, info.content, 0644) if err != nil { return err } } // Begin transaction - err = transaction.Create() + err := transaction.Create() if err != nil { return err } defer transaction.Remove() // Build ~/.vim/pack/volt dir - err = builder.Build(false) + err = builder.Build(false, cfg, lockJSON) if err != nil { return errors.Wrap(err, "could not build "+pathutil.VimVoltDir()) } diff --git a/subcmd/migrate/migrater.go b/usecase/migrate.go similarity index 83% rename from subcmd/migrate/migrater.go rename to usecase/migrate.go index 2e26ff20..a9b93b50 100644 --- a/subcmd/migrate/migrater.go +++ b/usecase/migrate.go @@ -1,13 +1,16 @@ -package migrate +package usecase import ( - "github.com/pkg/errors" "sort" + + "github.com/pkg/errors" + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/lockjson" ) // Migrater migrates many kinds of data. type Migrater interface { - Migrate() error + Migrate(cfg *config.Config, lockJSON *lockjson.LockJSON) error Name() string Description(brief bool) string } diff --git a/usecase/self_upgrade.go b/usecase/self_upgrade.go new file mode 100644 index 00000000..43b0da02 --- /dev/null +++ b/usecase/self_upgrade.go @@ -0,0 +1,166 @@ +package usecase + +import ( + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "syscall" + "time" + + "github.com/pkg/errors" + + "github.com/vim-volt/volt/httputil" + "github.com/vim-volt/volt/logger" +) + +// SelfUpgrade upgrades running volt binary if checkOnly = false. +// if checkOnly = true, only check the latest version and shows the information. +func SelfUpgrade(latestURL string, checkOnly bool) error { + // Check the latest binary info + release, err := checkLatest(latestURL) + if err != nil { + return err + } + logger.Debugf("tag_name = %q", release.TagName) + tagNameVer, err := ParseVersion(release.TagName) + if err != nil { + return err + } + if CompareVersion(tagNameVer, Version()) <= 0 { + logger.Info("No updates were found.") + return nil + } + logger.Infof("Found update: %s -> %s", VersionString(), release.TagName) + + // Show release note + fmt.Println("---") + fmt.Println(release.Body) + fmt.Println("---") + + if checkOnly { + return nil + } + + // Download the latest binary as "volt[.exe].latest" + voltExe, err := getExecutablePath() + if err != nil { + return err + } + latestFile, err := os.OpenFile(voltExe+".latest", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) + if err != nil { + return err + } + err = download(latestFile, release) + latestFile.Close() + if err != nil { + return err + } + + // Rename dir/volt[.exe] to dir/volt[.exe].old + // NOTE: Windows can rename running executable file + if err := os.Rename(voltExe, voltExe+".old"); err != nil { + return err + } + + // Rename dir/volt[.exe].latest to dir/volt[.exe] + if err := os.Rename(voltExe+".latest", voltExe); err != nil { + return err + } + + // Spawn dir/volt[.exe] with env "VOLT_SELF_UPGRADE_PPID={pid}" + voltCmd := exec.Command(voltExe, "self-upgrade") + if err = voltCmd.Start(); err != nil { + return err + } + return nil +} + +func download(w io.Writer, release *Release) error { + suffix := runtime.GOOS + "-" + runtime.GOARCH + for i := range release.Assets { + // e.g.: Name = "volt-v0.1.2-linux-amd64" + if strings.HasSuffix(release.Assets[i].Name, suffix) { + r, err := httputil.GetContentReader(release.Assets[i].BrowserDownloadURL) + if err != nil { + return err + } + defer r.Close() + if _, err = io.Copy(w, r); err != nil { + return err + } + break + } + } + return nil +} + +// checkLatest returns the latest release information. +func checkLatest(url string) (*Release, error) { + content, err := httputil.GetContent(url) + if err != nil { + return nil, err + } + var release Release + if err = json.Unmarshal(content, &release); err != nil { + return nil, err + } + return &release, nil +} + +func getExecutablePath() (string, error) { + exe, err := os.Executable() + if err != nil { + return "", err + } + return filepath.EvalSymlinks(exe) +} + +// Release has information about a volt release. +type Release struct { + TagName string `json:"tag_name"` + Body string `json:"body"` + Assets []struct { + BrowserDownloadURL string `json:"browser_download_url"` + Name string `json:"name"` + } +} + +// RemoveOldBinary removes old +func RemoveOldBinary(ppid int) error { + // Wait until the parent process exits + if died := waitUntilParentExits(ppid); !died { + return errors.Errorf("parent pid (%d) is keeping alive for long time", ppid) + } + + // Remove old binary + voltExe, err := getExecutablePath() + if err != nil { + return err + } + return os.Remove(voltExe + ".old") +} + +func waitUntilParentExits(pid int) bool { + fib := []int{1, 1, 2, 3, 5, 8, 13} // 33 second + for i := 0; i < len(fib); i++ { + if !processIsAlive(pid) { + return true + } + time.Sleep(time.Duration(fib[i]) * time.Second) + } + return false +} + +func processIsAlive(pid int) bool { + process, err := os.FindProcess(pid) + if err != nil { + return false + } + err = process.Signal(syscall.Signal(0)) + return err == nil +} diff --git a/subcmd/self_upgrade_test.go b/usecase/self_upgrade_test.go similarity index 71% rename from subcmd/self_upgrade_test.go rename to usecase/self_upgrade_test.go index 858dd344..1f37fffb 100644 --- a/subcmd/self_upgrade_test.go +++ b/usecase/self_upgrade_test.go @@ -1,10 +1,15 @@ -package subcmd +package usecase import ( "io/ioutil" "os" "strings" "testing" + + "github.com/pkg/errors" + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/gateway" + "github.com/vim-volt/volt/lockjson" ) func TestVoltSelfUpgrade(t *testing.T) { @@ -28,10 +33,9 @@ func testVoltSelfUpgradeCheckFromOldVer(t *testing.T) { // =============== run =============== // - var err *Error + var err *gateway.Error out := captureOutput(t, func() { - args := []string{"volt", "self-upgrade", "-check"} - err = Run(args, DefaultRunner) + err = runVolt(t, "self-upgrade", "-check") }) if err != nil { @@ -46,8 +50,7 @@ func testVoltSelfUpgradeCheckFromOldVer(t *testing.T) { func testVoltSelfUpgradeCheckFromCurrentVer(t *testing.T) { var err *Error out := captureOutput(t, func() { - args := []string{"volt", "self-upgrade", "-check"} - err = Run(args, DefaultRunner) + err = runVolt(t, "self-upgrade", "-check") }) if err != nil { @@ -58,6 +61,7 @@ func testVoltSelfUpgradeCheckFromCurrentVer(t *testing.T) { } } +// TODO use https://github.com/rhysd/go-fakeio func captureOutput(t *testing.T, f func()) string { r, w, err := os.Pipe() if err != nil { @@ -83,3 +87,23 @@ func captureOutput(t *testing.T, f func()) string { os.Stderr = oldStderr return <-outCh } + +func runVolt(t *testing.T, cmd string, args ...string) *Error { + c := LookUpCmd(cmd) + if c == nil { + t.Fatal("unknown command '" + cmd + "'") + } + lockJSON, err := lockjson.Read() + if err != nil { + t.Fatal(errors.Wrap(err, "failed to read lock.json").Error()) + } + cfg, err := config.Read() + if err != nil { + t.Fatal(errors.Wrap(err, "failed to read config.toml").Error()) + } + return c.Run(&CmdContext{ + Args: args, + LockJSON: lockJSON, + Config: cfg, + }) +} diff --git a/usecase/version.go b/usecase/version.go new file mode 100644 index 00000000..b56230ef --- /dev/null +++ b/usecase/version.go @@ -0,0 +1,77 @@ +package usecase + +import ( + "errors" + "regexp" + "strconv" +) + +// NOTE: this is not constant for testing (to change it temporarily) +var voltVersion = "v0.3.6-alpha" + +// VersionString is current version string +func VersionString() string { + return voltVersion +} + +// VersionInfo is [major, minor, patch, alphaBeta] +type VersionInfo []int + +const ( + suffixAlpha = 1 + suffixBeta = 2 + suffixStable = 9 +) + +var rxVersion = regexp.MustCompile(`^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(-alpha|-beta)?`) + +// Version returns versions [major, minor, patch, alphaBeta] +func Version() VersionInfo { + // parseVersion(voltVersion) must not return non-nil error! + voltVersionInfo, err := ParseVersion(voltVersion) + if err != nil { + panic(err) + } + return voltVersionInfo +} + +// CompareVersion compares two versions. +// and returns negative if v1 < v2, or positive if v1 > v2, or 0 if v1 == v2. +func CompareVersion(v1, v2 VersionInfo) int { + for i := 0; i < 4; i++ { + if v1[i] > v2[i] { + return 1 + } else if v1[i] < v2[i] { + return -1 + } + } + return 0 +} + +// ParseVersion parses version string +func ParseVersion(ver string) (VersionInfo, error) { + m := rxVersion.FindStringSubmatch(ver) + if len(m) == 0 { + return nil, errors.New("version number format is invalid: " + ver) + } + info := make(VersionInfo, 0, 4) + for i := 1; i <= 3 && m[i] != ""; i++ { + n, err := strconv.Atoi(m[i]) + if err != nil { + return nil, err + } + info = append(info, n) + } + if len(info) == 2 { + info = append(info, 0) + } + switch m[4] { + case "": + info = append(info, suffixStable) + case "-alpha": + info = append(info, suffixAlpha) + case "-beta": + info = append(info, suffixBeta) + } + return info, nil +} diff --git a/subcmd/version_test.go b/usecase/version_test.go similarity index 88% rename from subcmd/version_test.go rename to usecase/version_test.go index 13c51de9..3a7b7689 100644 --- a/subcmd/version_test.go +++ b/usecase/version_test.go @@ -1,4 +1,4 @@ -package subcmd +package usecase import "testing" @@ -36,13 +36,13 @@ func TestCompareVersion(t *testing.T) { } } -func parse(t *testing.T, ver string) versionInfo { +func parse(t *testing.T, ver string) VersionInfo { vinfo, err := parseVersion(ver) if err != nil { t.Errorf("\"%s\" should be a version number but isn't: %s", ver, err.Error()) } if len(vinfo) != 4 { - t.Errorf("parseVersion(%q) returned invalid versionInfo: %q", ver, vinfo) + t.Errorf("parseVersion(%q) returned invalid version info: %q", ver, vinfo) } return vinfo }