diff --git a/Gopkg.lock b/Gopkg.lock index 0554c19d..8d5e1aaf 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -60,6 +60,12 @@ packages = ["."] revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + [[projects]] branch = "master" name = "github.com/sergi/go-diff" @@ -189,6 +195,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "b1abfdb1122cba036acff15042e704f2621de2eb3348e5751a2eb4fe46112e4a" + inputs-digest = "184a72da18c6dd9f91e0525a9fe6a5e072c2758b9046233614da5c88483acece" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index 0e95e094..e1eaa951 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ See [the command reference](https://github.com/vim-volt/volt/blob/master/CMDREF. * Or `go get github.com/vim-volt/volt` * You need Go 1.9 or higher * And if you are using Windows Subsystem Linux, you need to apply **[the patch for os.RemoveAll()](https://go-review.googlesource.com/c/go/+/62970) ! ([#1](https://github.com/vim-volt/go-volt/issues/1))** - * But it's a hassle, you can just download linux-386/amd64 binaries from [GitHub releases](https://github.com/vim-volt/volt/releases) :) + * But it's a hassle, you can just download linux-(386/amd64) binaries from [GitHub releases](https://github.com/vim-volt/volt/releases) :) And there is bash completion script in [\_contrib](https://github.com/vim-volt/volt/blob/master/_contrib/completion/bash) directory (thanks @AvianY). @@ -119,12 +119,10 @@ You can update all plugins as follows: $ volt get -l -u ``` -`-l` works like all plugins in current profile are specified (the repositories list is read from `$VOLTPATH/lock.json`). -If you do not use profile feature, or `enable` and `disable` commands, you can -think that `-l` specifies all plugins what you have installed. `-u` updates specified plugins. +`-l` works like all plugins in current profile are specified (the repositories list is read from `$VOLTPATH/lock.json`). -Or, update only specified plugin(s) as follows: +Or you can update only specified plugin(s) as follows: ``` $ volt get -u tyru/caw.vim @@ -306,14 +304,7 @@ See [plugconf directory](https://github.com/tyru/dotfiles/tree/75a37b4a640a5cffe You can think this is similar feature of **branch** of `git`. The default profile name is "default". -You can see profile list by `volt profile list`. - -``` -$ volt profile list -* default -``` - -You can create a new profile by `volt profile new`. +You can create an *empty* profile by `volt profile new`. ``` $ volt profile new foo # will create profile "foo" @@ -322,7 +313,8 @@ $ volt profile list foo ``` -You can switch current profile by `volt profile set`. +Then you can switch current profile by `volt profile set`. +This removes all plugins from `~/.vim/pack/volt/opt/*`, because the new created profile is empty; no plugins are included. ``` $ volt profile set foo # will switch profile to "foo" @@ -331,38 +323,40 @@ $ volt profile list * foo ``` -You can delete profile by `volt profile destroy` (but you cannot delete current profile which you are switching on). +You can install new plugins or enable installed plugins **only in the current profile.** +`volt enable` is a shortcut of `volt profile add -current`. ``` -$ volt profile destroy foo # will delete profile "foo" +$ volt enable foo/bar bar/baz # enable installed plugins (foo/bar, bar/baz) also in new profile +$ volt profile add -current foo/bar bar/baz # same as above +$ volt get foo/bar bar/baz # or you can just use 'volt get', this installs missing plugins (it just includes plugins if already installed) ``` -You can enable/disable plugin by `volt enable` (`volt profile add`), `volt disable` (`volt profile rm`). +You can disable plugins by `volt disable`. +This is a shortcut of `volt profile rm -current`. ``` -$ volt enable tyru/caw.vim # enable loading tyru/caw.vim on current profile -$ volt profile add foo tyru/caw.vim # enable loading tyru/caw.vim on "foo" profile +$ volt disable foo/bar # disable loading foo/bar on current profile +$ volt profile rm -current foo/bar # same as above +$ volt profile rm foo foo/bar # or disable plugins outside current profile (of course 'volt profile add' can do it too) ``` +You can delete profile by `volt profile destroy` (but you cannot delete current profile which you are switching on). + ``` -$ volt disable tyru/caw.vim # disable loading tyru/caw.vim on current profile -$ volt profile rm foo tyru/caw.vim # disable loading tyru/caw.vim on "foo" profile +$ volt profile destroy foo # will delete profile "foo" ``` -You can create a vimrc & gvimrc file for each profile: +--- + +And you can create local vimrc & gvimrc files for each profile: * vimrc: `$VOLTPATH/rc//vimrc.vim` * gvimrc: `$VOLTPATH/rc//gvimrc.vim` -NOTE: If the path(s) exists, `$MYVIMRC` and `$MYGVIMRC` are set. So `:edit $MYVIMRC` does not open generated vimrc (`~/.vim/vimrc`), but above vimrc/gvimrc. +NOTE: If the path(s) exists, `$MYVIMRC` and `$MYGVIMRC` are set. So `:edit $MYVIMRC` opens above vimrc/gvimrc, not generated vimrc (`~/.vim/vimrc`). -This file is copied to `~/.vim/vimrc` and `~/.vim/gvimrc` with magic comment (shows error if existing vimrc/gvimrc files exist with no magic comment). - -And you can enable/disable vimrc by `volt profile use` (or you can simply remove `$VOLTPATH/rc//vimrc.vim` file if you don't want vimrc for the profile). - -``` -$ volt profile use -current vimrc false # Disable installing vimrc on current profile -$ volt profile use default gvimrc true # Enable installing gvimrc on profile default -``` +The files are copied to `~/.vim/vimrc` and `~/.vim/gvimrc` with magic comment. +Because volt shows an error if existing vimrc/gvimrc files exist with no magic comment which is not created by volt. See `volt help profile` for more detailed information. diff --git a/_docs/json-dsl.md b/_docs/json-dsl.md new file mode 100644 index 00000000..e5c62b94 --- /dev/null +++ b/_docs/json-dsl.md @@ -0,0 +1,816 @@ + +[Original (Japanese)](https://gist.github.com/tyru/819e593b2d996321298f6338bbaa34e0) + +# Volt refactoring note: JSON DSL and Transaction + +## Example of JSON DSL + +```json +["$label", + 1, + "installing plugins:", + ["$vimdir/with-install", + ["$parallel", + ["$label", + 2, + " github.com/tyru/open-browser.vim ... {{if .Done}}done!{{end}}", + ["$parallel", + ["lockjson/add", + ["repos/get", "github.com/tyru/open-browser.vim"], + ["$array", "default"]], + ["plugconf/install", "github.com/tyru/open-browser.vim"]]], + ["$label", + 3, + " github.com/tyru/open-browser-github.vim ... {{if .Done}}done!{{end}}", + ["$parallel", + ["lockjson/add", + ["repos/get", "github.com/tyru/open-browser-github.vim"], + ["$array", "default"]], + ["plugconf/install", + "github.com/tyru/open-browser-github.vim"]]]]]] +``` + +## Wordings + +* operator: "callable" object of DSL. this is generic name of function and macro +* function: the name of process + * e.g. "label" + * e.g. "parallel" +* macro: like function, but is expanded before execution + * e.g. "$array" +* expression: the form of operator application + * e.g. `["label", ...]` + * e.g. `["parallel", ...]` +* transaction log (file): a JSON file which is saved at + `$VOLTPATH/trx/{id}/log.json` + +## Goals + +This refactoring allows us or makes it easy to implement the following issues: + +1. JSON file of AST (abstract syntax tree) is saved under `$VOLTPATH/trx/{id}/` +2. The history feature (undo, redo, list, ...) like `yum history` + [#147](https://github.com/vim-volt/volt/issues/147) + * `volt history undo` executes `[$invert, expr]` for transaction log + * `volt history redo` just executes saved expression in transaction log +3. Display progress bar [#118](https://github.com/vim-volt/volt/issues/188) + * Updating progress bars according to `["label", ...]` expression +4. `volt watch` command can be easy to implement + [#174](https://github.com/vim-volt/volt/issues/174) + * Current `volt build` implementation installs all repositories of current + profile, not specific repositories +5. Parallelism + * Currently each command independently implements it using goroutine, but DSL + provides higher level parallel processing +6. More detailed unit testing + * Small component is easy to test + * And especially "Subcmd layer" is easy because it does not access to + filesystem +7. Gracefully rollback when an error occurs while processing a DSL [#200](https://github.com/vim-volt/volt/issues/200) + +## Layered architecture + +The volt commands like `volt get` which may modify lock.json, config.toml([#221](https://github.com/vim-volt/volt/issues/221)), +filesystem, are executed in several steps: + +1. (Gateway layer): pass subcommand arguments, lock.json & config.toml structure + to Subcmd layer +2. (Subcmd layer): Create an AST according to given information + * This layer cannot touch filesystem, because it makes unit testing difficult +3. (DSL layer): Execute the AST. This note mainly describes this layer's design + +Below is the dependency graph: + +``` +Gateway --> Subcmd --> DSL +``` + +* Gateway only depends Subcmd +* Subcmd doesn't know Gateway +* Subcmd only depends DSL +* DSL doesn't know Subcmd + +## Abstract + +JSON DSL is a S-expression like DSL represented as JSON format. + +```json +["op", "arg1", "arg2"] +``` + +This is an application form (called "expression" in this note). +An array literal value is written using `$array` operator. + +```json +["$array", 1, 2, 3] +``` + +This expression is evaluated to `[1, 2, 3]`. + +Each expression has 0 or more parameters. And evaluation +strategy is an eager evaluation. + +Parameter types are + +* JSON types + * boolean + * string + * number + * array + * object +* lambda +* expression (only macro can treat this) + +But all values must be able to be serialized to JSON. Because AST of whole +process is serialized and saved as a "transaction log file". + +NOTE: All macros has `$` prefixed name for readability. +Macros are not saved in transaction log (expanded before saving). + +The process can be rolled back, when an error occur while the process, or user +send SIGINT signal, or `volt history undo` command is executed. The transaction +log file does not have ID but the saved directory `{id}` does: + +``` +$VOLTPATH/trx/{id}/log.json +``` + +`{id}` is called transaction ID, a simple serial number assigned `max + 1` like +DB's AUTOINCREMENT. + +JSON DSL has the following characteristic: + +* Idempotent +* Invertible + +## Idempotent + +All operators have an idempotency: "even if an expression is executed twice, it +guarantees the existence (not the content) of a requested resource." + +One also might think that "why the it defines the existence, not content?" Because, if we +define operator's idempotency as "after an expression was executed twice at +different times, lock.json, filesystem must be the same." But `volt get A` +installs the latest plugin of remote repository. At the first time and second +time, the repository's HEAD may be different. But it guarantees that the +existence of specified property of lock.json, and the repository on filesystem. + +Here is a more concrete example: + +1. Install plugin A, B, C by `volt get A B C`. +2. Uninstall plugin B. +3. Re-run 1's operation by `volt history redo {id}` + +At 3, one might think that "3 should raise an error because plugin B is already +uninstalled!" But volt does raise an error, because operators when uninstalling +(`repos/delete`, `lockjson/remove`, `plugconf/delete`) does nothing if given +plugin does not exist, like HTTP's DELETE method. Those operator guarantees +that "the specified resource is deleted after the execution." + +## Invertible + +All operators have an inverse expression. Here is the example of JSON DSL when +[tyru/caw.vim](https://github.com/tyru/caw.vim) plugin is installed (it may be +different with latest volt's JSON DSL when installing. this is a simplified +version for easiness). + +```json +["$vimdir/with-install", + ["$do", + ["lockjson/add", + ["repos/get", "github.com/tyru/caw.vim"], + ["$array", "default"]], + ["plugconf/install", "github.com/tyru/caw.vim"]]] +``` + +At first, expands all macros (`["$array", "default"]` cannot be written in JSON +notation, it is expanded to array but array literal is `["$array", "default"]`). + +```json +["vimdir/with-install", + ["fn", [], + ["do", + ["fn", [], + ["lockjson/add", + ["repos/get", "github.com/tyru/caw.vim"], + ["$array", "default"]]], + ["fn", [], + ["plugconf/install", "github.com/tyru/caw.vim"]]]]] +``` + +I show you what happens in several steps when you "invert" the expression like +`volt history undo`. + +At first, to invert the expression, `$invert` macro is used: + +```json +["$invert", + ["vimdir/with-install", + ["fn", [], + ["do", + ["fn", [], + ["lockjson/add", + ["repos/get", "github.com/tyru/caw.vim"], + ["$array", "default"]]], + ["fn", [], + ["plugconf/install", "github.com/tyru/caw.vim"]]]]]] +``` + +`["$invert", ["vimdir/with-install", thunk]]` is expanded to +`["vimdir/with-install", ["$invert", thunk]]`. Internally, it is implemented as +calling `InvertExpr()` method of `vimdir/with-install` operator struct. + +```json +["vimdir/with-install", + ["$invert", + ["fn", [], + ["do", + ["fn", [], + ["lockjson/add", + ["repos/get", "github.com/tyru/caw.vim"], + ["$array", "default"]]], + ["fn", [], + ["plugconf/install", "github.com/tyru/caw.vim"]]]]]] +``` + +And `["$invert", ["fn", args, body]]` becomes +`["fn", args, ["$invert", body]]`. + +```json +["vimdir/with-install", + ["fn", [], + ["$invert", + ["do", + ["fn", [], + ["lockjson/add", + ["repos/get", "github.com/tyru/caw.vim"], + ["$array", "default"]]], + ["fn", [], + ["plugconf/install", "github.com/tyru/caw.vim"]]]]]] +``` + +And `["$invert", ["do", thunk1, thunk2]]` becomes +`["do", ["$invert", thunk2], ["$invert", thunk1]]`. +Note that `thunk1` and `thunk2` become reversed order. + +```json +["vimdir/with-install", + ["fn", [], + ["do", + ["fn", [], + ["$invert", + ["lockjson/add", + ["repos/get", "github.com/tyru/caw.vim"], + ["$array", "default"]]]], + ["fn", [], + ["$invert", + ["plugconf/install", "github.com/tyru/caw.vim"]]]]]] +``` + +And +* `["$invert", ["lockjson/add", repos, profiles]]` becomes + `["lockjson/remove", ["$invert", repos], ["$invert", profiles]]` +* `["$invert", ["plugconf/install", repos]]` becomes + `["plugconf/delete", ["$invert", repos]]` + +```json +["vimdir/with-install", + ["fn", [], + ["do", + ["fn", [], + ["lockjson/add", + ["$invert", ["repos/get", "github.com/tyru/caw.vim"]], + ["$invert", ["$array", "default"]]]], + ["fn", [], + ["plugconf/install", ["$invert", "github.com/tyru/caw.vim"]]]]]] +``` + +`["$invert", ["repos/get", path]]` becomes +`["repos/delete", ["$invert", path]]`. + +```json +["vimdir/with-install", + ["fn", [], + ["do", + ["fn", [], + ["lockjson/add", + ["repos/delete", ["$invert", "github.com/tyru/caw.vim"]], + ["$invert", ["$array", "default"]]]], + ["fn", [], + ["plugconf/install", ["$invert", "github.com/tyru/caw.vim"]]]]]] +``` + +And if `$invert` is applied to literals like string, JSON array, it just remains +as-is. + +```json +["vimdir/with-install", + ["fn", [], + ["do", + ["fn", [], + ["lockjson/add", + ["repos/delete", "github.com/tyru/caw.vim"], + ["$array", "default"]]], + ["fn", [], + ["plugconf/install", "github.com/tyru/caw.vim"]]]]] +``` + +We can successfully evaluate the inverse expression of the first expression! :) + +### Uninstall operation should be "Recovable"? + +If `volt history undo {id}` takes uninstall operation, it executes `git clone` +to install plugin(s) from remote. But should it be recovable like uninstall +operation just "archive" to specific directory, and `volt history undo {id}` +"unarchive" the repository? + +I don't think it is what a user wants to do. I think, when a user undoes +uninstall operation, it should just clone new repositories from remote. A user +just wants repositories to be back, of the latest one not the old version. + +It is possible to design to map commands to archive/unarchive operations (I +thought it in the initial design). But I found it is redundant. + +## Update operation must be Recovable! + +TODO + +* `"repos/git/fetch"` +* `"repos/git/update"` + +## The implementation of an operator + +To achieve goals as mentioned above, +the signature of Go function of an operator is: + +```go +func (ctx Context, args []Value) (ret Value, rollback func(), err error) +``` + +* [Go context](https://golang.org/pkg/context/) makes graceful rollback easy. +* `Value` is the interface which is serializable to JSON. + All types of JSON DSL must implement this interface. +* `rollback` function is to rollback this operator's process. + Invoking rollback function after invoking this operator function + must rollback lock.json, config.toml, filesystem to the previous state. + * TODO: inverse expression may be enough for rollback? + +## Operator responsibility + +As the above signature shows, operators must take care the following points: + +1. is cancellable with given context, because Go's context is not a magic to + make it cancellable if receiving it as an argument :) +2. must rollback to the previous state invoking rollback function + * TODO: inverse expression may be enough for rollback? +3. must guarantee idempotency: it must not destroy user environment if + expression is executed twice +4. must have an inverse expression (not inverse operator) + +## JSON DSL API + +TODO: Move to Godoc. + +### Basic operators + +* `["$array", v1 Value, ...] Array` + * Returns inverse expression of given expression. + * Internally, this macro calls `InvertExpr()` method of each operator struct. + * What value is returned depends on each operator's `InvertExpr()` + implementation. + +* `["$invert", expr Value] Value` + * Returns inverse expression of given expression. + * Internally, this macro calls `InvertExpr()` method of each operator struct. + * What value is returned depends on each operator's `InvertExpr()` + implementation. + +* `["$eval", expr Value] Value` + * Evaluate `expr` at parsing time. + This is useful to save evaluated value to transaction log, + instead of its expression. + * See `repos/git/fetch`, `repos/git/update` for concrete example. + +* `["fn", [args ...[]string], body Expr[R]]R` + * Returns a lambda with `arity` number arguments and `body` expression. + * e.g. + * `["fn", [], ["lockjson/write"]]` + * see below + +```json +[["fn", [["path", "string"], ["profiles", ["array", "string"]]], + ["lockjson/add", ["repos/get", ["arg", "path"]], ["arg", "profiles"]]], + "github.com/tyru/caw.vim", + ["$array", "default"]] +``` + +* `["$label", linenum number, tmpl string, expr Expr[R]] R` + * `["$label", linenum, tmpl, expr]` expands to + `["label", linenum, tmpl, ["fn", [] expr]]` + +* `["label", linenum number, tmpl string, thunk Func[R]] R` + * Render `tmpl` by text/template to `linenum` line (1-origin). + Returns the evaluated value of `thunk`. + * e.g. + * `["$invert", ["label", linenum, "msg", thunk]]` = `["label", ["$invert", linenum], "revert: \"msg\"", ["$invert", thunk]]` + * See `Label examples` section for more details + +* `["$do", expr1 Expr[R1], ..., expr_last Expr[R2]] R2` + * `["$do", expr1, expr2]` expands to + `["do", ["fn", [], expr1], ["fn", [], expr2]]` + +* `["do", thunk1 R1, ..., thunk_last R2] R2` + * Executes multiple lambdas in series. + * Returns the evaluated value of the last lambda. + * e.g. + * `["$invert", ["do", thunk1, thunk2]]` = `["do", ["$invert", thunk1], ["$invert", thunk2]]` + * Note that the arguments are reversed. + +* `["$parallel", expr1 Expr[R1], ..., expr_last Expr[R2]] R2` + * `["$parallel", expr1, expr2]` expands to + `["parallel", ["fn", [], expr1], ["fn", [], expr2]]` + +* `["parallel", thunk1 Func[R1], ..., thunk_last Func[R2]] R2` + * Executes multiple lambdas in parallel. + * Returns the evaluated value of the last lambda. + * e.g. + * `["$invert", ["parallel", thunk1, thunk2]]` = `["parallel", ["$invert", thunk2], ["$invert", thunk1]]` + * The arguments are **not** reversed because parallel does not care of + execution order of given value. + +### Repository operators + +* `["repos/get", path ReposPath] Repos` + * If the repository does not exist, executes `git clone` on given `path` + repository and saves to `$VOLTPATH/repos/{path}`. + * If the repository already exists, returns the repository information. + The information is the repository information on filesystem, not in lock.json. + * `volt get A` emits `["lockjson/add", ["repos/get", path], profiles]` + * If A is git repository, it updates lock.json information with repository + information. + * If A is static repository, it does nothing. + * e.g. + * `["repos/get", "github.com/tyru/caw.vim"]` + * `["$invert", ["repos/get", path]]` = `["repos/delete", ["$invert", path]]` + +* `["repos/delete", path ReposPath] Repos` + * If the repository does not exist, it does nothing. + * If the repository already exists, returns the repository information. + The information is the repository information on filesystem, not in lock.json. + * `volt rm -r A` emits `["lockjson/add", ["repos/delete", path], profiles]` + * `volt rm A` emits `["lockjson/add", ["repos/info", path], profiles]` + * If A is git repository, it deletes `path` repository's directory. + * If A is static repository, shows warning `static repository cannot be + deleted by 'volt rm' command. delete '{path}' manually` + * To avoid removing local repository accidentally. + * e.g. + * `["repos/delete", "github.com/tyru/caw.vim"]` + * `["$invert", ["repos/delete", path]]` = `["repos/get", ["$invert", path]]` + +* `["repos/info", path ReposPath] Repos` + * Returns `path` repository information. + * e.g. + * `["lockjson/add", ["repos/get", path], profiles]` + * `volt rm A` emits this expression. + * `["$invert", ["repos/info", path]]` = `["repos/info", ["$invert", path]]` + +* `["repos/git/fetch", path ReposPath] head_hash string` + * Executes `git fetch` on `path` repository. + Returns the hash string of HEAD. + * For bare repository, the result HEAD hash string is the hash string of + default branch's HEAD. + * e.g. + * `["$invert", ["repos/git/fetch", path]]` = `["repos/git/fetch", ["$invert", path]]` + +* `["repos/git/update", path ReposPath, target_hash string, prev_hash string] void` + * This fails if the working tree is dirty. + * If `target_hash` is not merged yet in current branch, try `git merge + --ff-only` (it raises an error if cannot merge with fast-forward) + * If `target_hash` is already merged in current branch, try `git reset --hard + {target_hash}` + * It does nothing for bare git repository. + * e.g. + * `["repos/git/update", "github.com/tyru/caw.vim", ["$eval", ["repos/git/rev-parse", "HEAD", "github.com/tyru/caw.vim"]], ["$eval", ["repos/git/fetch", "github.com/tyru/caw.vim"]]]` + * To save evaluated hash string in transaction log instead of its + expression, apply `$eval` to `repos/git/fetch` expression. + * `["$invert", ["repos/git/update", path, target_hash, prev_hash]]` = `["repos/git/update", ["$invert", path], ["$invert", prev_hash], ["$invert", target_hash]]` + +* `["repos/git/rev-parse", str string, path ReposPath] hash string` + * Returns hash string from `str` argument. This executes `git rev-parse + {str}` on `path` repository. + * e.g. + * `["repos/git/rev-parse", "HEAD", "github.com/tyru/caw.vim"]` + * `["$invert", ["repos/git/rev-parse", str, path]]` = `["repos/git/rev-parse", ["$invert", str], ["$invert", path]]` + +### lock.json operators + +* `["lockjson/write"] void` + * Writes lock.json to a file + * e.g. + * `["$invert", ["lockjson/write"]]` = `["lockjson/write"]` + +* `["lockjson/add", repos Repos, profiles []string]` + * Add `repos` information to `repos[]` array in lock.json. + If `profiles` is not empty, the repository name is added to + specified profile (`profiles[]` array in lock.json). + * It fails if specified profile name does not exist. + * Need to create profile before using `lockjson/profile/add`. + * e.g. + * `["lockjson/add", ["repos/get", "github.com/tyru/caw.vim"], ["$array", "default"]]` + * `["$invert", ["lockjson/add", repos, profiles]]` = `["lockjson/remove", ["$invert", repos], ["$invert", profiles]]` + +* `["lockjson/profile/add", name string] Profile` + * Add empty profile named `name` if it does not exist. + If it exists, do nothing. + Returns created/existed profile. + * e.g. + * `["lockjson/profile/add", "default"]` + * `["$invert", ["lockjson/profile/add", name]]` = `["lockjson/profile/remove", ["$invert", name]]` + +* `["lockjson/profile/remove", name string] Profile` + * Remove specified profile named `name` if it exists. + If it does not exist, do nothing. + Returns removed profile. + * e.g. + * `["lockjson/profile/remove", "default"]` + * `["$invert", ["lockjson/profile/remove", name]]` = `["lockjson/profile/add", ["$invert", name]]` + +### Plugconf operators + +* `["plugconf/install", path ReposPath] void` + * Created plugconf of specified repository, or fetch a plugconf file from + [vim-volt/plugconf-templates](https://github.com/vim-volt/plugconf-templates) + * e.g. + * `["plugconf/install", "github.com/tyru/caw.vim"]` + * `["$invert", ["plugconf/install", path]]` = `["plugconf/delete", ["$invert", path]]` + +* `["plugconf/delete", path ReposPath] void` + * Delete a plugconf file of `path`. + If it does not exist, do nothing. + * e.g. + * `["plugconf/delete", "github.com/tyru/caw.vim"]` + * `["$invert", ["plugconf/delete", path]]` = `["plugconf/install", ["$invert", path]]` + +### Migration operators + +* `["migrate/plugconf/config-func"] void` + * Converts `s:config()` function name to `s:on_load_pre()` in all plugconf files. + * See `volt migrate -help plugconf/config-func` for the details. + * e.g. + * `["$invert", ["migrate/plugconf/config-func"]]` = `["migrate/plugconf/config-func"]` + +### Vim directory operators + +* `["vimdir/with-install", paths "all" | []ReposPath, expr Expr[R]] R` + * `paths` is the list of repositories to build after `expr` is executed. + * `"all"` means all repositories of current profile. + * e.g. + * `["$invert", ["vimdir/with-install", paths, expr]]` = `["vimdir/with-install", ["$invert", paths], ["$invert", expr]]` + * See "Why `vimdir/install` and `vimdir/uninstall` operators do not exist?" + section + +#### Why `vimdir/install` and `vimdir/uninstall` operators do not exist? + +We'll describe why `vimdir/install` and `vimdir/uninstall` operators do not +exist, and `vimdir/with-install` exists instead. + +For example, now we have the following expression with `vimdir/uninstall`. It +removes lock.json, deletes repository, plugconf, and also the repository in vim +directory: + +```json +[ + "do", + ["lockjson/remove", + { + "type": "git", + "path": "github.com/tyru/caw.vim", + "version": "deadbeefcafebabe" + }, + ["$array", "default"] + ], + ["repos/delete", "github.com/tyru/caw.vim"], + ["plugconf/delete", "github.com/tyru/caw.vim"], + ["vimdir/uninstall", "github.com/tyru/caw.vim"] +] +``` + +And below is the inverse expression of above. + +```json +[ + "do", + ["vimdir/install", "github.com/tyru/caw.vim"], + ["plugconf/install", "github.com/tyru/caw.vim"], + ["repos/get", "github.com/tyru/caw.vim"], + ["lockjson/add", + { + "type": "git", + "path": "github.com/tyru/caw.vim", + "version": "deadbeefcafebabe" + }, + ["$array", "default"] + ] +] +``` + +1. Installs the repository to vim directory + **EVEN THE REPOSITORY DOES NOT EXIST YET!** +2. Installs plugconf +3. Clones repository +4. Add repository information to lock.json + +1 must raise an error! +The problem is that `["$invert", ["do", exprs...]]` simply reverses the `exprs`. +We have to install **always** the repository to vim directory after all +expressions. + +This is what we expected. + +```json +["vimdir/with-install", + ["github.com/tyru/caw.vim"], + ["do", + ["lockjson/remove", + { + "type": "git", + "path": "github.com/tyru/caw.vim", + "version": "deadbeefcafebabe" + }, + ["$array", "default"] + ], + ["repos/delete", "github.com/tyru/caw.vim"], + ["plugconf/delete", "github.com/tyru/caw.vim"]]] +``` + +The inverse expression of the above is: + +```json +["vimdir/with-install", + ["github.com/tyru/caw.vim"], + ["do", + ["plugconf/install", "github.com/tyru/caw.vim"], + ["repos/get", "github.com/tyru/caw.vim"], + ["lockjson/add", + { + "type": "git", + "path": "github.com/tyru/caw.vim", + "version": "deadbeefcafebabe" + }, + ["$array", "default"]]]] +``` + +1. Installs plugconf +2. Clones repository +3. Add repository information to lock.json +4. Installs the repository to vim directory (yes!) + +We successfully installs [tyru/caw.vim](https://github.com/tyru/caw.vim) +plugin :) + +But, of course if we placed `vimdir/with-install` at before `repos/delete` or +`plugconf/delete` not at top-level. + +```json +["do", + ["vimdir/with-install", + ["github.com/tyru/caw.vim"], + "dummy"], + ["lockjson/remove", + { + "type": "git", + "path": "github.com/tyru/caw.vim", + "version": "deadbeefcafebabe" + }, + ["$array", "default"] + ], + ["repos/delete", "github.com/tyru/caw.vim"], + ["plugconf/delete", "github.com/tyru/caw.vim"]] +``` + +```json +["do", + ["plugconf/install", "github.com/tyru/caw.vim"], + ["repos/get", "github.com/tyru/caw.vim"], + ["lockjson/add", + { + "type": "git", + "path": "github.com/tyru/caw.vim", + "version": "deadbeefcafebabe" + }, + ["$array", "default"]], + ["vimdir/with-install", + ["github.com/tyru/caw.vim"], + "dummy"]] +``` + +But a user does not touch JSON DSL. In other words, constructing "wrong" AST +must not occur without Volt's bug. + +### Install examples + +Here is the simple JSON to install +[tyru/caw.vim](https://github.com/tyru/caw.vim) using Git. + +```json +["vimdir/with-install", + ["do", + ["lockjson/add", + ["repos/get", "github.com/tyru/caw.vim"], + ["$array", "default"]], + ["plugconf/install", "github.com/tyru/caw.vim"]]] +``` + +Here is the inverse expression of above. + +```json +["vimdir/with-install", + ["do", + ["plugconf/delete", "github.com/tyru/caw.vim"], + ["lockjson/remove", + ["repos/delete", "github.com/tyru/caw.vim"], + ["$array", "default"]]]] +``` + +Here is the JSON to install plugins from local directory (static repository). + +```json +["vimdir/with-install", + ["lockjson/add", + { ... (repository information of local directory) ... }, + ["$array", "default"]], + ["plugconf/install", "localhost/local/myplugin"]] +``` + +Here is the inverse expression of above. + +```json +["vimdir/with-install", + ["plugconf/delete", "localhost/local/myplugin"], + ["lockjson/remove", + { ... (repository information of local directory) ... }, + ["$array", "default"]]] +``` + +### Label examples + +Here is the simple example of installing +[tyru/caw.vim](https://github.com/tyru/caw.vim) plugin. + +```json +["label", + 1, + "installing github.com/caw.vim...", + ["vimdir/with-install", + ["do", + ["lockjson/add", + ["repos/get", "github.com/tyru/caw.vim"], + ["$array", "default"]], + ["plugconf/install", "github.com/tyru/caw.vim"]]]] +``` + +Here is the inverse expression of above. +Note that: + +* Message becomes `revert "%s"` +* `$invert` is applied to the third argument, thus the argument of `do` is + inverted + +```json +["label", + 1, + "revert \"installing github.com/caw.vim...\"", + ["vimdir/with-install", + ["do", + ["plugconf/install", "github.com/tyru/caw.vim"], + ["lockjson/add", + ["repos/get", "github.com/tyru/caw.vim"], + ["$array", "default"]]]]] +``` + +Here is more complex example to install two plugins "tyru/open-browser.vim", +"tyru/open-browser-github.vim". Two levels of `label` expression exist. + +```json +["label", + 1, + "installing plugins:", + ["vimdir/with-install", + ["parallel", + ["label", + 2, + " github.com/tyru/open-browser.vim ... {{if .Done}}done!{{end}}", + ["parallel", + ["lockjson/add", + ["repos/get", "github.com/tyru/open-browser.vim"], + ["$array", "default"]], + ["plugconf/install", "github.com/tyru/open-browser.vim"]]], + ["label", + 3, + " github.com/tyru/open-browser-github.vim ... {{if .Done}}done!{{end}}", + ["parallel", + ["lockjson/add", + ["repos/get", "github.com/tyru/open-browser-github.vim"], + ["$array", "default"]], + ["plugconf/install", "github.com/tyru/open-browser-github.vim"]]]]]] +``` diff --git a/subcmd/buildinfo/buildinfo.go b/buildinfo/buildinfo.go similarity index 86% rename from subcmd/buildinfo/buildinfo.go rename to buildinfo/buildinfo.go index 281e06cf..9cc03e40 100644 --- a/subcmd/buildinfo/buildinfo.go +++ b/buildinfo/buildinfo.go @@ -9,14 +9,18 @@ import ( "github.com/vim-volt/volt/pathutil" ) +// BuildInfo is a struct for build-info.json, which saves the cache information +// of 'volt build'. type BuildInfo struct { Repos ReposList `json:"repos"` Version int64 `json:"version"` Strategy string `json:"strategy"` } +// ReposList = []Repos type ReposList []Repos +// Repos is a struct for repository information of build-info.json type Repos struct { Type lockjson.ReposType `json:"type"` Path pathutil.ReposPath `json:"path"` @@ -25,9 +29,10 @@ type Repos struct { DirtyWorktree bool `json:"dirty_worktree,omitempty"` } -// key: filepath, value: version +// FileMap is a map[string]string (key: filepath, value: version) type FileMap map[string]string +// Read reads build-info.json func Read() (*BuildInfo, error) { // Return initial build-info.json struct // if the file does not exist @@ -84,6 +89,7 @@ func (buildInfo *BuildInfo) validate() error { return nil } +// FindByReposPath finds reposPath from reposList func (reposList *ReposList) FindByReposPath(reposPath pathutil.ReposPath) *Repos { for i := range *reposList { repos := &(*reposList)[i] @@ -94,6 +100,7 @@ func (reposList *ReposList) FindByReposPath(reposPath pathutil.ReposPath) *Repos return nil } +// RemoveByReposPath removes reposPath from reposList func (reposList *ReposList) RemoveByReposPath(reposPath pathutil.ReposPath) { for i := range *reposList { repos := &(*reposList)[i] diff --git a/config/config.go b/config/config.go index fc8feedf..94019f83 100644 --- a/config/config.go +++ b/config/config.go @@ -9,20 +9,20 @@ import ( // Config is marshallable content of config.toml type Config struct { - Alias map[string][]string `toml:"alias"` - Build configBuild `toml:"build"` - Get configGet `toml:"get"` + Alias map[string][]string `toml:"alias" json:"alias"` + Build configBuild `toml:"build" json:"build"` + Get configGet `toml:"get" json:"get"` } // configBuild is a config for 'volt build'. type configBuild struct { - Strategy string `toml:"strategy"` + Strategy string `toml:"strategy" json:"strategy"` } // configGet is a config for 'volt get'. type configGet struct { - CreateSkeletonPlugconf *bool `toml:"create_skeleton_plugconf"` - FallbackGitCmd *bool `toml:"fallback_git_cmd"` + CreateSkeletonPlugconf *bool `toml:"create_skeleton_plugconf" json:"create_skeleton_plugconf"` + FallbackGitCmd *bool `toml:"fallback_git_cmd" json:"fallback_git_cmd"` } const ( diff --git a/dsl/deparse/deparse.go b/dsl/deparse/deparse.go new file mode 100644 index 00000000..0c50dfa2 --- /dev/null +++ b/dsl/deparse/deparse.go @@ -0,0 +1,50 @@ +package deparse + +import ( + "github.com/pkg/errors" + "github.com/vim-volt/volt/dsl/ops" + "github.com/vim-volt/volt/dsl/types" +) + +// Deparse deparses types.Expr. +// ["@", 1, 2, 3] becomes [1, 2, 3] +func Deparse(value types.Value) (interface{}, error) { + if value.Type() == types.NullType { + return nil, nil + } + switch val := value.(type) { + case types.Bool: + return val.Value(), nil + case types.String: + return val.Value(), nil + case types.Number: + return val.Value(), nil + case types.Object: + result := make(map[string]interface{}, len(val.Value())) + for k, o := range val.Value() { + v, err := Deparse(o) + if err != nil { + return nil, err + } + result[k] = v + } + return result, nil + case types.Expr: + args := val.Args() + result := make([]interface{}, 0, len(args)+1) + // Do not include "@" in array literal + if val.Op().String() != ops.ArrayOp.String() { + result = append(result, val.Op().String()) + } + for i := range args { + v, err := Deparse(args[i]) + if err != nil { + return nil, err + } + result = append(result, v) + } + return result, nil + default: + return nil, errors.Errorf("unknown value was given '%+v'", val) + } +} diff --git a/dsl/dslctx/dslctx.go b/dsl/dslctx/dslctx.go new file mode 100644 index 00000000..df4d9499 --- /dev/null +++ b/dsl/dslctx/dslctx.go @@ -0,0 +1,64 @@ +package dslctx + +import ( + "context" + "errors" + + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/lockjson" +) + +// KeyType is the type of the key of context specified for Execute() +type KeyType uint + +const ( + // TrxIDKey is the key to get transaction ID + TrxIDKey KeyType = iota + // LockJSONKey is the key to get *lockjson.LockJSON value + LockJSONKey + // ConfigKey is the key to get *config.Config value + ConfigKey +) + +// WithDSLValues adds given values +func WithDSLValues(ctx context.Context, lockJSON *lockjson.LockJSON, cfg *config.Config) context.Context { + ctx = context.WithValue(ctx, LockJSONKey, lockJSON) + ctx = context.WithValue(ctx, ConfigKey, cfg) + return ctx +} + +// Validate validates if required keys exist in ctx +func Validate(ctx context.Context) error { + for _, required := range []struct { + key KeyType + validate func(interface{}) error + }{ + {LockJSONKey, validateLockJSON}, + {ConfigKey, validateConfig}, + } { + if err := required.validate(ctx.Value(required.key)); err != nil { + return err + } + } + return nil +} + +func validateLockJSON(v interface{}) error { + if v == nil { + return errors.New("no lock.json key in context") + } + if _, ok := v.(*lockjson.LockJSON); !ok { + return errors.New("invalid lock.json data in context") + } + return nil +} + +func validateConfig(v interface{}) error { + if v == nil { + return errors.New("no config.toml key in context") + } + if _, ok := v.(*config.Config); !ok { + return errors.New("invalid config.toml data in context") + } + return nil +} diff --git a/dsl/execute.go b/dsl/execute.go new file mode 100644 index 00000000..3e5137c5 --- /dev/null +++ b/dsl/execute.go @@ -0,0 +1,158 @@ +package dsl + +import ( + "bytes" + "context" + "encoding/json" + "io" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/dsl/deparse" + "github.com/vim-volt/volt/dsl/dslctx" + "github.com/vim-volt/volt/dsl/ops/util" + "github.com/vim-volt/volt/dsl/types" + "github.com/vim-volt/volt/lockjson" + "github.com/vim-volt/volt/pathutil" + "github.com/vim-volt/volt/transaction" +) + +// Execute executes given expr with given ctx. +func Execute(ctx context.Context, expr types.Expr) (_ types.Value, result error) { + if err := dslctx.Validate(ctx); err != nil { + return nil, err + } + + // Begin transaction + trx, err := transaction.Start() + if err != nil { + return nil, err + } + defer func() { + if err := trx.Done(); err != nil { + result = err + } + }() + + // Expand all macros before write + expr, err = expandMacro(expr) + if err != nil { + return nil, errors.Wrap(err, "failed to expand macros") + } + + // Write given expression to $VOLTPATH/trx/lock/log.json + err = writeTrxLog(ctx, expr) + if err != nil { + return nil, err + } + + val, rollback, err := evalDepthFirst(ctx, expr) + if err != nil { + if rollback != nil { + rollback(ctx) + } + return nil, errors.Wrap(err, "expression returned an error") + } + return val, nil +} + +func evalDepthFirst(ctx context.Context, expr types.Expr) (_ types.Value, _ func(context.Context), result error) { + op := expr.Op() + g := util.FuncGuard(op.String()) + defer func() { + result = g.Error(recover()) + }() + + // Evaluate arguments first + args := expr.Args() + newArgs := make([]types.Value, 0, len(args)) + for i := range args { + innerExpr, ok := args[i].(types.Expr) + if !ok { + newArgs = append(newArgs, args[i]) + continue + } + ret, rbFunc, err := evalDepthFirst(ctx, innerExpr) + g.Add(rbFunc) + if err != nil { + return nil, g.Rollback, g.Error(err) + } + newArgs = append(newArgs, ret) + } + + ret, rbFunc, err := op.EvalExpr(ctx, newArgs) + g.Add(rbFunc) + return ret, g.Rollback, g.Error(err) +} + +func expandMacro(expr types.Expr) (types.Expr, error) { + val, err := doExpandMacro(expr) + if err != nil { + return nil, err + } + result, ok := val.(types.Expr) + if !ok { + return nil, errors.New("the result of expansion of macros must be an expression") + } + return result, nil +} + +// doExpandMacro expands macro's expression recursively +func doExpandMacro(expr types.Expr) (types.Value, error) { + op := expr.Op() + if !op.IsMacro() { + return expr, nil + } + args := expr.Args() + for i := range args { + if inner, ok := args[i].(types.Expr); ok { + v, err := doExpandMacro(inner) + if err != nil { + return nil, err + } + args[i] = v + } + } + // XXX: should we care rollback function? + val, _, err := op.EvalExpr(context.Background(), args) + return val, err +} + +func writeTrxLog(ctx context.Context, expr types.Expr) (result error) { + deparsed, err := deparse.Deparse(expr) + if err != nil { + return errors.Wrap(err, "failed to deparse expression") + } + + type contentT struct { + Expr interface{} `json:"expr"` + Config *config.Config `json:"config"` + LockJSON *lockjson.LockJSON `json:"lockjson"` + } + content, err := json.Marshal(&contentT{ + Expr: deparsed, + Config: ctx.Value(dslctx.ConfigKey).(*config.Config), + LockJSON: ctx.Value(dslctx.LockJSONKey).(*lockjson.LockJSON), + }) + if err != nil { + return errors.Wrap(err, "failed to marshal as JSON") + } + + filename := filepath.Join(pathutil.TrxDir(), "lock", "log.json") + logFile, err := os.Create(filename) + if err != nil { + return errors.Wrapf(err, "could not create %s", filename) + } + defer func() { + if err := logFile.Close(); err != nil { + result = errors.Wrapf(err, "failed to close transaction log %s", filename) + } + }() + _, err = io.Copy(logFile, bytes.NewReader(content)) + if err != nil { + return errors.Wrapf(err, "failed to write transaction log %s", filename) + } + return nil +} diff --git a/dsl/ops/func.go b/dsl/ops/func.go new file mode 100644 index 00000000..30e17d65 --- /dev/null +++ b/dsl/ops/func.go @@ -0,0 +1,11 @@ +package ops + +type funcBase string + +func (f *funcBase) String() string { + return string(*f) +} + +func (*funcBase) IsMacro() bool { + return false +} diff --git a/dsl/ops/func_do.go b/dsl/ops/func_do.go new file mode 100644 index 00000000..a33a0c39 --- /dev/null +++ b/dsl/ops/func_do.go @@ -0,0 +1,63 @@ +package ops + +import ( + "context" + + "github.com/vim-volt/volt/dsl/ops/util" + "github.com/vim-volt/volt/dsl/types" +) + +func init() { + opsMap[DoOp.String()] = DoOp +} + +type doOp struct { + funcBase +} + +// DoOp is "do" operation +var DoOp = &doOp{funcBase("do")} + +func (*doOp) Bind(args ...types.Value) (types.Expr, error) { + thunkType := types.NewLambdaType(types.AnyValue) + sig := make([]types.Type, 0, len(args)) + for i := 0; i < len(args); i++ { + sig = append(sig, thunkType) + } + if err := util.Signature(sig...).Check(args); err != nil { + return nil, err + } + retType := args[len(args)-1].Type() + return types.NewExpr(DoOp, args, retType), nil +} + +func (*doOp) InvertExpr(ctx context.Context, args []types.Value) (types.Value, error) { + newargs := make([]types.Value, len(args)) + for i := range args { + a, err := args[i].Invert(ctx) + if err != nil { + return nil, err + } + newargs[len(args)-i] = a + } + return DoOp.Bind(newargs...) +} + +func (*doOp) EvalExpr(ctx context.Context, args []types.Value) (_ types.Value, _ func(context.Context), result error) { + g := util.FuncGuard(DoOp.String()) + defer func() { + result = g.Error(recover()) + }() + + var lastVal types.Value + for i := range args { + v, rbFunc, err := args[i].(types.Lambda).Call(ctx) + g.Add(rbFunc) + if err != nil { + result = g.Error(err) + return + } + lastVal = v + } + return lastVal, g.Rollback, nil +} diff --git a/dsl/ops/func_lockjson_write.go b/dsl/ops/func_lockjson_write.go new file mode 100644 index 00000000..25bc097d --- /dev/null +++ b/dsl/ops/func_lockjson_write.go @@ -0,0 +1,44 @@ +package ops + +import ( + "context" + + "github.com/pkg/errors" + "github.com/vim-volt/volt/dsl/dslctx" + "github.com/vim-volt/volt/dsl/ops/util" + "github.com/vim-volt/volt/dsl/types" + "github.com/vim-volt/volt/lockjson" +) + +func init() { + opsMap[LockJSONWriteOp.String()] = LockJSONWriteOp +} + +type lockJSONWriteOp struct { + funcBase +} + +// LockJSONWriteOp is "lockjson/write" operator +var LockJSONWriteOp = &lockJSONWriteOp{funcBase("lockjson/write")} + +func (*lockJSONWriteOp) Bind(args ...types.Value) (types.Expr, error) { + if err := util.Signature().Check(args); err != nil { + return nil, err + } + retType := types.VoidType + return types.NewExpr(LockJSONWriteOp, args, retType), nil +} + +func (*lockJSONWriteOp) InvertExpr(_ context.Context, args []types.Value) (types.Value, error) { + return LockJSONWriteOp.Bind(args...) +} + +func (*lockJSONWriteOp) EvalExpr(ctx context.Context, args []types.Value) (_ types.Value, _ func(context.Context), result error) { + lockJSON := ctx.Value(dslctx.LockJSONKey).(*lockjson.LockJSON) + result = lockJSON.Write() + if result != nil { + result = errors.Wrap(result, "could not write to lock.json") + } + + return +} diff --git a/dsl/ops/func_migrate_plugconf_config_func.go b/dsl/ops/func_migrate_plugconf_config_func.go new file mode 100644 index 00000000..f0d8d59b --- /dev/null +++ b/dsl/ops/func_migrate_plugconf_config_func.go @@ -0,0 +1,106 @@ +package ops + +import ( + "bytes" + "context" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/dsl/dslctx" + "github.com/vim-volt/volt/dsl/ops/util" + "github.com/vim-volt/volt/dsl/types" + "github.com/vim-volt/volt/lockjson" + "github.com/vim-volt/volt/pathutil" + "github.com/vim-volt/volt/plugconf" + "github.com/vim-volt/volt/subcmd/builder" +) + +func init() { + opsMap[MigratePlugconfConfigFuncOp.String()] = MigratePlugconfConfigFuncOp +} + +type migratePlugconfConfigFuncOp struct { + funcBase +} + +// MigratePlugconfConfigFuncOp is "migrate/plugconf/config-func" operator +var MigratePlugconfConfigFuncOp = &migratePlugconfConfigFuncOp{ + funcBase("migrate/plugconf/config-func"), +} + +func (*migratePlugconfConfigFuncOp) Bind(args ...types.Value) (types.Expr, error) { + if err := util.Signature().Check(args); err != nil { + return nil, err + } + retType := types.VoidType + return types.NewExpr(MigratePlugconfConfigFuncOp, args, retType), nil +} + +func (*migratePlugconfConfigFuncOp) InvertExpr(_ context.Context, args []types.Value) (types.Value, error) { + return MigratePlugconfConfigFuncOp.Bind(args...) +} + +func (*migratePlugconfConfigFuncOp) EvalExpr(ctx context.Context, args []types.Value) (_ types.Value, _ func(context.Context), result error) { + lockJSON := ctx.Value(dslctx.LockJSONKey).(*lockjson.LockJSON) + cfg := ctx.Value(dslctx.ConfigKey).(*config.Config) + + parseResults, parseErr := plugconf.ParseMultiPlugconf(lockJSON.Repos) + if parseErr.HasErrs() { + var errMsg bytes.Buffer + errMsg.WriteString("Please fix the following errors before migration:") + for _, err := range parseErr.Errors().Errors { + for _, line := range strings.Split(err.Error(), "\n") { + errMsg.WriteString(" ") + errMsg.WriteString(line) + } + } + result = errors.New(errMsg.String()) + return + } + + type plugInfo struct { + path string + content []byte + } + infoList := make([]plugInfo, 0, len(lockJSON.Repos)) + + // Collects plugconf infomations and check errors + parseResults.Each(func(reposPath pathutil.ReposPath, info *plugconf.ParsedInfo) { + if !info.ConvertConfigToOnLoadPreFunc() { + return // no s:config() function + } + content, err := info.GeneratePlugconf() + if err != nil { + result = errors.Wrap(err, "could not generate converted plugconf") + return + } + infoList = append(infoList, plugInfo{ + path: reposPath.Plugconf(), + content: content, + }) + }) + if result != nil { + return + } + + // 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) + if err != nil { + result = errors.Wrapf(err, "could not write to file %s", info.path) + return + } + } + + // Build ~/.vim/pack/volt dir + result = builder.Build(false, lockJSON, cfg) + if result != nil { + result = errors.Wrap(result, "could not build "+pathutil.VimVoltDir()) + } + return +} diff --git a/dsl/ops/lookup.go b/dsl/ops/lookup.go new file mode 100644 index 00000000..fbc6803f --- /dev/null +++ b/dsl/ops/lookup.go @@ -0,0 +1,13 @@ +package ops + +import "github.com/vim-volt/volt/dsl/types" + +// opsMap holds all operation structs. +// All operations in dsl/op/*.go sets its struct to this in init() +var opsMap = make(map[string]types.Op) + +// Lookup looks up operator name +func Lookup(name string) (types.Op, bool) { + op, exists := opsMap[name] + return op, exists +} diff --git a/dsl/ops/macro.go b/dsl/ops/macro.go new file mode 100644 index 00000000..07421bd1 --- /dev/null +++ b/dsl/ops/macro.go @@ -0,0 +1,25 @@ +package ops + +import ( + "context" + + "github.com/vim-volt/volt/dsl/types" +) + +type macroBase string + +func (m *macroBase) String() string { + return string(*m) +} + +func (*macroBase) IsMacro() bool { + return true +} + +// macroInvertExpr inverts the result of op.Execute() which expands an expression +func (*macroBase) macroInvertExpr(ctx context.Context, val types.Value, _ func(context.Context), err error) (types.Value, error) { + if err != nil { + return nil, err + } + return val.Invert(ctx) +} diff --git a/dsl/ops/macro_array.go b/dsl/ops/macro_array.go new file mode 100644 index 00000000..1c96b65a --- /dev/null +++ b/dsl/ops/macro_array.go @@ -0,0 +1,32 @@ +package ops + +import ( + "context" + + "github.com/vim-volt/volt/dsl/types" +) + +func init() { + opsMap[ArrayOp.String()] = ArrayOp +} + +type arrayOp struct { + macroBase +} + +// ArrayOp is "$array" operator +var ArrayOp = &arrayOp{macroBase("$array")} + +func (op *arrayOp) InvertExpr(ctx context.Context, args []types.Value) (types.Value, error) { + val, rollback, err := op.EvalExpr(ctx, args) + return op.macroInvertExpr(ctx, val, rollback, err) +} + +func (*arrayOp) Bind(args ...types.Value) (types.Expr, error) { + expr := types.NewExpr(ArrayOp, args, types.NewArrayType(types.AnyValue)) + return expr, nil +} + +func (*arrayOp) EvalExpr(ctx context.Context, args []types.Value) (types.Value, func(context.Context), error) { + return types.NewArray(args, types.AnyValue), nil, nil +} diff --git a/dsl/ops/macro_eval.go b/dsl/ops/macro_eval.go new file mode 100644 index 00000000..9c22f7a9 --- /dev/null +++ b/dsl/ops/macro_eval.go @@ -0,0 +1,36 @@ +package ops + +import ( + "context" + + "github.com/vim-volt/volt/dsl/ops/util" + "github.com/vim-volt/volt/dsl/types" +) + +func init() { + opsMap[EvalOp.String()] = EvalOp +} + +type evalOp struct { + macroBase +} + +// EvalOp is "$eval" operator +var EvalOp = &evalOp{macroBase("$eval")} + +func (op *evalOp) InvertExpr(ctx context.Context, args []types.Value) (types.Value, error) { + val, rollback, err := op.EvalExpr(ctx, args) + return op.macroInvertExpr(ctx, val, rollback, err) +} + +func (*evalOp) Bind(args ...types.Value) (types.Expr, error) { + expr := types.NewExpr(ArrayOp, args, types.NewArrayType(types.AnyValue)) + return expr, nil +} + +func (*evalOp) EvalExpr(ctx context.Context, args []types.Value) (types.Value, func(context.Context), error) { + if err := util.Signature(types.AnyValue).Check(args); err != nil { + return nil, nil, err + } + return args[0].Eval(ctx) +} diff --git a/dsl/ops/macro_invert.go b/dsl/ops/macro_invert.go new file mode 100644 index 00000000..c40a7248 --- /dev/null +++ b/dsl/ops/macro_invert.go @@ -0,0 +1,37 @@ +package ops + +import ( + "context" + + "github.com/vim-volt/volt/dsl/ops/util" + "github.com/vim-volt/volt/dsl/types" +) + +func init() { + opsMap[InvertOp.String()] = InvertOp +} + +type invertOp struct { + macroBase +} + +// InvertOp is "$invert" operator +var InvertOp = &invertOp{macroBase("$invert")} + +func (op *invertOp) InvertExpr(ctx context.Context, args []types.Value) (types.Value, error) { + val, rollback, err := op.EvalExpr(ctx, args) + return op.macroInvertExpr(ctx, val, rollback, err) +} + +func (*invertOp) Bind(args ...types.Value) (types.Expr, error) { + expr := types.NewExpr(ArrayOp, args, types.NewArrayType(types.AnyValue)) + return expr, nil +} + +func (*invertOp) EvalExpr(ctx context.Context, args []types.Value) (types.Value, func(context.Context), error) { + if err := util.Signature(types.AnyValue).Check(args); err != nil { + return nil, nil, err + } + val, err := args[0].Invert(ctx) + return val, nil, err +} diff --git a/dsl/ops/util/guard.go b/dsl/ops/util/guard.go new file mode 100644 index 00000000..edc17587 --- /dev/null +++ b/dsl/ops/util/guard.go @@ -0,0 +1,66 @@ +package util + +import ( + "context" + "fmt" + + "github.com/pkg/errors" +) + +// Guard invokes "rollback functions" if Rollback method received non-nil value +// (e.g. recover(), non-nil error). +type Guard interface { + // Error sets v as an error if v is non-nil. + // This returns the error. + // + // defer func() { + // result = g.Error(recover()) + // }() + // + // // or + // + // if err != nil { + // return g.Error(err) + // } + // + Error(v interface{}) error + + // Rollback calls rollback functions in reversed order + Rollback(ctx context.Context) + + // Add adds given rollback functions, but skips if f == nil + Add(f func(context.Context)) +} + +// FuncGuard returns Guard instance for function +func FuncGuard(name string) Guard { + return &guard{errMsg: fmt.Sprintf("function \"%s\" has an error", name)} +} + +type guard struct { + errMsg string + err error + rbFuncs []func(context.Context) +} + +func (g *guard) Error(v interface{}) error { + if err, ok := v.(error); ok { + g.err = errors.Wrap(err, g.errMsg) + } else if v != nil { + g.err = errors.Wrap(fmt.Errorf("%s", v), g.errMsg) + } + return g.err +} + +func (g *guard) Rollback(ctx context.Context) { + for i := len(g.rbFuncs) - 1; i >= 0; i-- { + g.rbFuncs[i](ctx) + } + g.rbFuncs = nil // do not rollback twice +} + +func (g *guard) Add(f func(context.Context)) { + if f != nil { + g.rbFuncs = append(g.rbFuncs, f) + } +} diff --git a/dsl/ops/util/sigcheck.go b/dsl/ops/util/sigcheck.go new file mode 100644 index 00000000..b180b67f --- /dev/null +++ b/dsl/ops/util/sigcheck.go @@ -0,0 +1,34 @@ +package util + +import ( + "fmt" + + "github.com/vim-volt/volt/dsl/types" +) + +// SigChecker checks if the type of args met given types to Signature() +type SigChecker interface { + Check(args []types.Value) error +} + +// Signature returns SigChecker for given types +func Signature(argTypes ...types.Type) SigChecker { + return &sigChecker{argTypes: argTypes} +} + +type sigChecker struct { + argTypes []types.Type +} + +func (sc *sigChecker) Check(args []types.Value) error { + if len(args) != len(sc.argTypes) { + return fmt.Errorf("expected %d arity but got %d", len(sc.argTypes), len(args)) + } + for i := range sc.argTypes { + if !args[i].Type().InstanceOf(sc.argTypes[i]) { + return fmt.Errorf("expected %s instance but got %s", + sc.argTypes[i].String(), args[i].Type().String()) + } + } + return nil +} diff --git a/dsl/parse/parse.go b/dsl/parse/parse.go new file mode 100644 index 00000000..d8cd7e93 --- /dev/null +++ b/dsl/parse/parse.go @@ -0,0 +1,92 @@ +package dsl + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/vim-volt/volt/dsl/ops" + "github.com/vim-volt/volt/dsl/types" +) + +// Parse parses expr JSON. And if an array literal value is found: +// 1. Split to operation and its arguments +// 2. Do semantic analysis recursively for its arguments +// 3. Convert to *Expr +func Parse(content []byte) (types.Expr, error) { + var value interface{} + if err := json.Unmarshal(content, value); err != nil { + return nil, err + } + array, ok := value.([]interface{}) + if !ok { + return nil, errors.New("top-level must be an array") + } + arrayValue, err := parseArray(array) + if err != nil { + return nil, err + } + // If expression's operator is a macro, return value may not be an array + // (e.g. ["macro", 1, 2]) + expr, ok := arrayValue.(types.Expr) + if !ok { + return nil, errors.New("the result must be an expression") + } + return expr, nil +} + +func parseArray(array []interface{}) (types.Value, error) { + if len(array) == 0 { + return nil, errors.New("expected operation but got an empty array") + } + opName, ok := array[0].(string) + if !ok { + return nil, fmt.Errorf("expected operator (string) but got '%+v'", array[0]) + } + args := make([]types.Value, 0, len(array)-1) + for i := 1; i < len(array); i++ { + v, err := parse(array[i]) + if err != nil { + return nil, err + } + args = append(args, v) + } + op, exists := ops.Lookup(opName) + if !exists { + return nil, fmt.Errorf("no such operation '%s'", opName) + } + // Expand macro's expression at parsing time + if op.IsMacro() { + val, _, err := op.EvalExpr(context.Background(), args) + return val, err + } + return op.Bind(args...) +} + +func parse(value interface{}) (types.Value, error) { + switch val := value.(type) { + case nil: + return types.NullValue, nil + case bool: + return types.NewBool(val), nil + case string: + return types.NewString(val), nil + case float64: + return types.NewNumber(val), nil + case map[string]interface{}: + m := make(map[string]types.Value, len(val)) + for k, o := range m { + v, err := parse(o) + if err != nil { + return nil, err + } + m[k] = v + } + return types.NewObject(m, types.AnyValue), nil + case []interface{}: + return parseArray(val) + default: + return nil, fmt.Errorf("unknown value was given '%+v'", val) + } +} diff --git a/dsl/types/expr.go b/dsl/types/expr.go new file mode 100644 index 00000000..10845b15 --- /dev/null +++ b/dsl/types/expr.go @@ -0,0 +1,52 @@ +package types + +import "context" + +// Expr has an operation and its arguments +type Expr interface { + Value + + // Op returns operator + Op() Op + + // Args returns arguments + Args() []Value + + // RetType returns return type + RetType() Type +} + +// NewExpr creates Expr instance +func NewExpr(op Op, args []Value, retType Type) Expr { + return &expr{op: op, args: args, retType: retType} +} + +type expr struct { + op Op + args []Value + retType Type +} + +func (expr *expr) Op() Op { + return expr.op +} + +func (expr *expr) Args() []Value { + return expr.args +} + +func (expr *expr) RetType() Type { + return expr.retType +} + +func (expr *expr) Eval(ctx context.Context) (val Value, rollback func(context.Context), err error) { + return expr.op.EvalExpr(ctx, expr.args) +} + +func (expr *expr) Invert(ctx context.Context) (Value, error) { + return expr.op.InvertExpr(ctx, expr.args) +} + +func (expr *expr) Type() Type { + return expr.retType +} diff --git a/dsl/types/json.go b/dsl/types/json.go new file mode 100644 index 00000000..e8fde755 --- /dev/null +++ b/dsl/types/json.go @@ -0,0 +1,212 @@ +package types + +import "context" + +// ================ Null ================ + +// NullValue is the JSON null value +var NullValue = &nullT{} + +type nullT struct{} + +func (*nullT) Invert(context.Context) (Value, error) { + return NullValue, nil +} + +func (v *nullT) Eval(context.Context) (val Value, rollback func(context.Context), err error) { + return v, nil, nil +} + +func (*nullT) Type() Type { + return NullType +} + +// ================ Bool ================ + +// TrueValue is the JSON true value +var TrueValue = &boolT{true} + +// FalseValue is the JSON false value +var FalseValue = &boolT{false} + +// Bool is JSON boolean value +type Bool interface { + Value + + // Value returns the holding internal value + Value() bool +} + +// NewBool creates Bool instance +func NewBool(value bool) Bool { + if value { + return TrueValue + } + return FalseValue +} + +type boolT struct { + value bool +} + +func (v *boolT) Value() bool { + return v.value +} + +func (v *boolT) Invert(context.Context) (Value, error) { + return v, nil +} + +func (v *boolT) Eval(context.Context) (val Value, rollback func(context.Context), err error) { + return v, nil, nil +} + +func (*boolT) Type() Type { + return BoolType +} + +// ================ Number ================ + +// Number is JSON number value +type Number interface { + Value + + // Value returns the holding internal value + Value() float64 +} + +// NewNumber creates Number instance +func NewNumber(value float64) Number { + return &numberT{value: value} +} + +type numberT struct { + value float64 +} + +func (v *numberT) Value() float64 { + return v.value +} + +func (v *numberT) Invert(context.Context) (Value, error) { + return v, nil +} + +func (v *numberT) Eval(context.Context) (val Value, rollback func(context.Context), err error) { + return v, nil, nil +} + +func (*numberT) Type() Type { + return NumberType +} + +// ================ String ================ + +// String is JSON string value +type String interface { + Value + + // Value returns the holding internal value + Value() string +} + +// NewString creates String instance +func NewString(value string) String { + return &stringT{value: value} +} + +type stringT struct { + value string +} + +func (v *stringT) Value() string { + return v.value +} + +func (v *stringT) Invert(context.Context) (Value, error) { + return v, nil +} + +func (v *stringT) Eval(context.Context) (val Value, rollback func(context.Context), err error) { + return v, nil, nil +} + +func (*stringT) Type() Type { + return StringType +} + +// ================ Array ================ + +// Array is JSON array value +type Array interface { + Value + + // Value returns the holding internal value. + // DO NOT CHANGE THE RETURN VALUE DIRECTLY! + // Copy the slice before changing the value. + Value() []Value +} + +// NewArray creates Array instance +func NewArray(value []Value, argType Type) Array { + return &arrayT{value: value, typ: NewArrayType(argType)} +} + +type arrayT struct { + value []Value + typ Type +} + +func (v *arrayT) Value() []Value { + return v.value +} + +func (v *arrayT) Invert(context.Context) (Value, error) { + return v, nil +} + +func (v *arrayT) Eval(context.Context) (val Value, rollback func(context.Context), err error) { + return v, nil, nil +} + +func (v *arrayT) Type() Type { + return v.typ +} + +// ================ Object ================ + +// Object is JSON object value +type Object interface { + Value + + // Value returns the holding internal value. + // DO NOT CHANGE THE RETURN VALUE DIRECTLY! + // Copy the map instance before changing the value. + Value() map[string]Value +} + +// NewObject creates Object instance +func NewObject(value map[string]Value, argType Type) Object { + return &objectT{value: value, typ: NewObjectType(argType)} +} + +type objectT struct { + value map[string]Value + typ Type +} + +func (v *objectT) Value() map[string]Value { + return v.value +} + +func (v *objectT) Invert(context.Context) (Value, error) { + return v, nil +} + +func (v *objectT) Eval(context.Context) (val Value, rollback func(context.Context), err error) { + return v, nil, nil +} + +func (v *objectT) Type() Type { + return v.typ +} diff --git a/dsl/types/lambda.go b/dsl/types/lambda.go new file mode 100644 index 00000000..35c5e469 --- /dev/null +++ b/dsl/types/lambda.go @@ -0,0 +1,92 @@ +package types + +import ( + "context" + + "github.com/pkg/errors" +) + +// Lambda can be applicable, and it has an expression to execute. +type Lambda interface { + Value + + // Call calls this lambda with given args + Call(ctx context.Context, args ...Value) (Value, func(context.Context), error) +} + +type argT Array + +// ArgsDef is passed to builder function, the argument of NewLambda() +type ArgsDef struct { + args []argT +} + +// Define returns placeholder expression of given argument +func (def *ArgsDef) Define(n int, name String, typ Type) (Value, error) { + if n <= 0 { + return nil, errors.New("the number of argument must be positive") + } + for n > len(def.args) { + def.args = append(def.args, nil) + } + if def.args[n-1] != nil { + return nil, errors.Errorf("the %dth argument is already taken", n) + } + argExpr := []Value{NewString("arg"), name} + def.args[n-1] = NewArray(argExpr, AnyValue) + return def.args[n-1], nil +} + +// Inject replaces expr of ["arg", expr] with given values +func (def *ArgsDef) Inject(args []Value) error { + if len(args) != len(def.args) { + return errors.Errorf("expected %d arity but got %d", len(def.args), len(args)) + } + for i := range args { + if def.args[i] == nil { + return errors.Errorf("%dth arg is not taken", i+1) + } + def.args[i].Value()[1] = args[i] + } + return nil +} + +// NewLambda creates lambda value. +// Signature must have 1 type at least for a return type. +func NewLambda(builder func(*ArgsDef) (Expr, []Type, error)) (Lambda, error) { + def := &ArgsDef{args: make([]argT, 0)} + expr, sig, err := builder(def) + if err != nil { + return nil, errors.Wrap(err, "builder function returned an error") + } + return &lambdaT{ + def: def, + expr: expr, + typ: NewLambdaType(sig[0], sig[1:]...), + }, nil +} + +type lambdaT struct { + def *ArgsDef + expr Expr + typ Type +} + +func (v *lambdaT) Call(ctx context.Context, args ...Value) (Value, func(context.Context), error) { + if err := v.def.Inject(args); err != nil { + return nil, nil, err + } + return v.expr.Eval(ctx) +} + +func (v *lambdaT) Invert(context.Context) (Value, error) { + return v, nil +} + +func (v *lambdaT) Eval(context.Context) (Value, func(context.Context), error) { + return v, nil, nil +} + +func (v *lambdaT) Type() Type { + return v.typ +} diff --git a/dsl/types/op.go b/dsl/types/op.go new file mode 100644 index 00000000..565ed96a --- /dev/null +++ b/dsl/types/op.go @@ -0,0 +1,24 @@ +package types + +import "context" + +// Op is an operator of JSON DSL +type Op interface { + // String returns function name + String() string + + // InvertExpr returns inverted expression + InvertExpr(ctx context.Context, args []Value) (Value, error) + + // Bind binds its arguments, and check if the types of values are correct + Bind(args ...Value) (Expr, error) + + // EvalExpr evaluates expression (this operator + given arguments). + // If this operator is a function, it executes the operation and returns its + // result and error. + // If this operator is a macro, this expands expression. + EvalExpr(ctx context.Context, args []Value) (ret Value, rollback func(context.Context), err error) + + // IsMacro returns true if this operator is a macro + IsMacro() bool +} diff --git a/dsl/types/types.go b/dsl/types/types.go new file mode 100644 index 00000000..394d37c9 --- /dev/null +++ b/dsl/types/types.go @@ -0,0 +1,205 @@ +package types + +import ( + "bytes" +) + +// Type is a type of a value +type Type interface { + // String returns a string like "" + String() string + + // InstanceOf checks has-a relation with its argument type + InstanceOf(Type) bool +} + +// ===================== Void type ===================== // + +// VoidType is a void type +var VoidType = &voidType{} + +type voidType struct{} + +func (*voidType) String() string { + return "Void" +} + +func (*voidType) InstanceOf(t Type) bool { + if _, ok := t.(*voidType); ok { + return true + } + return false +} + +// ===================== Null type ===================== // + +// NullType is a null type +var NullType = &nullType{} + +type nullType struct{} + +func (*nullType) String() string { + return "Null" +} + +func (*nullType) InstanceOf(t Type) bool { + if _, ok := t.(*nullType); ok { + return true + } + return false +} + +// ===================== Bool type ===================== // + +// BoolType is a null type +var BoolType = &boolType{} + +type boolType struct{} + +func (*boolType) String() string { + return "Bool" +} + +func (*boolType) InstanceOf(t Type) bool { + if _, ok := t.(*boolType); ok { + return true + } + return false +} + +// ===================== Number type ===================== // + +// NumberType is a null type +var NumberType = &numberType{} + +type numberType struct{} + +func (*numberType) String() string { + return "Number" +} + +func (*numberType) InstanceOf(t Type) bool { + if _, ok := t.(*numberType); ok { + return true + } + return false +} + +// ===================== String type ===================== // + +// StringType is a null type +var StringType = &stringType{} + +type stringType struct{} + +func (*stringType) String() string { + return "String" +} + +func (*stringType) InstanceOf(t Type) bool { + if _, ok := t.(*stringType); ok { + return true + } + return false +} + +// ===================== Array type ===================== // + +// NewArrayType creates array type instance +func NewArrayType(arg Type) Type { + return &arrayType{arg: arg} +} + +type arrayType struct { + arg Type +} + +func (t *arrayType) String() string { + return "Array[" + t.arg.String() + "]" +} + +func (t *arrayType) InstanceOf(t2 Type) bool { + array, ok := t2.(*arrayType) + if !ok { + return false + } + return t.arg.InstanceOf(array.arg) +} + +// ===================== Object type ===================== // + +// NewObjectType creates object type instance +func NewObjectType(arg Type) Type { + return &objectType{arg: arg} +} + +type objectType struct { + arg Type +} + +func (t *objectType) String() string { + return "Object[" + t.arg.String() + "]" +} + +func (t *objectType) InstanceOf(t2 Type) bool { + object, ok := t2.(*objectType) + if !ok { + return false + } + return t.arg.InstanceOf(object.arg) +} + +// ===================== Any type ===================== // + +// AnyValue allows any type +var AnyValue = &anyType{} + +type anyType struct{} + +func (*anyType) String() string { + return "Any" +} + +func (*anyType) InstanceOf(_ Type) bool { + return true +} + +// ===================== Lambda type ===================== // + +// NewLambdaType creates lambda type instance. +// Signature must have 1 type at least for a return type. +func NewLambdaType(t Type, rest ...Type) Type { + signature := append([]Type{t}, rest...) + return &lambdaType{signature: signature} +} + +type lambdaType struct { + signature []Type +} + +func (t *lambdaType) String() string { + var arg bytes.Buffer + for i := range t.signature { + if i > 0 { + arg.WriteString(",") + } + arg.WriteString(t.signature[i].String()) + } + return "Lambda[" + arg.String() + "]" +} + +func (t *lambdaType) InstanceOf(t2 Type) bool { + lambda, ok := t2.(*lambdaType) + if !ok { + return false + } + if len(t.signature) != len(lambda.signature) { + return false + } + for i := range t.signature { + if !t.signature[i].InstanceOf(lambda.signature[i]) { + return false + } + } + return true +} diff --git a/dsl/types/value.go b/dsl/types/value.go new file mode 100644 index 00000000..0f716b85 --- /dev/null +++ b/dsl/types/value.go @@ -0,0 +1,19 @@ +package types + +import "context" + +// Value is JSON value +type Value interface { + // Invert returns inverted value/operation. + // All type values are invertible. + // Literals like string,number,... return itself as-is. + // If argument type or arity is different, this returns non-nil error. + Invert(ctx context.Context) (Value, error) + + // Eval returns a evaluated value. + // Literals like string,number,... return itself as-is. + Eval(ctx context.Context) (val Value, rollback func(context.Context), err error) + + // Type returns the type of this value. + Type() Type +} diff --git a/subcmd/cmd.go b/gateway/gateway.go similarity index 52% rename from subcmd/cmd.go rename to gateway/gateway.go index 9e1a7983..51d43cbe 100644 --- a/subcmd/cmd.go +++ b/gateway/gateway.go @@ -1,48 +1,28 @@ -package subcmd +package gateway import ( "errors" - "flag" "os" "os/user" "runtime" "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/lockjson" "github.com/vim-volt/volt/logger" + "github.com/vim-volt/volt/subcmd" ) -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 -} +type RunnerFunc func(c subcmd.Cmd, runctx *subcmd.RunContext) *subcmd.Error // DefaultRunner simply runs command with args -func DefaultRunner(c Cmd, args []string) *Error { - return c.Run(args) +func DefaultRunner(c subcmd.Cmd, runctx *subcmd.RunContext) *subcmd.Error { + return c.Run(runctx) } // Run is invoked by main(), each argument means 'volt {subcmd} {args}'. -func Run(args []string, cont RunnerFunc) *Error { +func Run(args []string, cont RunnerFunc) *subcmd.Error { if os.Getenv("VOLT_DEBUG") != "" { logger.SetLevel(logger.DebugLevel) } @@ -50,41 +30,55 @@ func Run(args []string, cont RunnerFunc) *Error { if len(args) <= 1 { args = append(args, "help") } - subCmd := args[1] + cmdname := args[1] args = args[2:] + // Read config.toml + cfg, err := config.Read() + if err != nil { + err = errors.New("could not read config.toml: " + err.Error()) + return &subcmd.Error{Code: 2, Msg: err.Error()} + } + // Expand subcommand alias - subCmd, args, err := expandAlias(subCmd, args) + cmdname, args, err = expandAlias(cmdname, args, cfg) if err != nil { - return &Error{Code: 1, Msg: err.Error()} + return &subcmd.Error{Code: 1, Msg: err.Error()} } - c, exists := cmdMap[subCmd] + c, exists := subcmd.LookupSubcmd(cmdname) if !exists { - return &Error{Code: 3, Msg: "Unknown command '" + subCmd + "'"} + return &subcmd.Error{Code: 3, Msg: "Unknown command '" + cmdname + "'"} } // 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 &subcmd.Error{Code: 4, Msg: err.Error()} } } - return cont(c, args) -} - -func expandAlias(subCmd string, args []string) (string, []string, error) { - cfg, err := config.Read() + // Read lock.json + lockJSON, err := lockjson.Read() if err != nil { - return "", nil, errors.New("could not read config.toml: " + err.Error()) + err = errors.New("failed to read lock.json: " + err.Error()) + return &subcmd.Error{Code: 5, Msg: err.Error()} } - if newArgs, exists := cfg.Alias[subCmd]; exists && len(newArgs) > 0 { - subCmd = newArgs[0] + + return cont(c, &subcmd.RunContext{ + Args: args, + LockJSON: lockJSON, + Config: cfg, + }) +} + +func expandAlias(cmdname string, args []string, cfg *config.Config) (string, []string, error) { + if newArgs, exists := cfg.Alias[cmdname]; exists && len(newArgs) > 0 { + cmdname = newArgs[0] args = append(newArgs[1:], args...) } - return subCmd, args, nil + return cmdname, args, nil } // On Windows, this function always returns nil. diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 0b474987..69e27726 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -37,10 +37,12 @@ func init() { os.RemoveAll(filepath.Join(testdataDir, "voltpath")) } +// TestdataDir returns the fullpath of "testdata" directory func TestdataDir() string { return testdataDir } +// SetUpEnv sets up environment variables related to volt. func SetUpEnv(t *testing.T) { tempDir, err := ioutil.TempDir("", "volt-test-") if err != nil { @@ -58,12 +60,16 @@ func SetUpEnv(t *testing.T) { } } +// RunVolt invokes volt command by os/exec.Command() and returns cmd.CombinedOutput() func RunVolt(args ...string) ([]byte, error) { cmd := exec.Command(voltCommand, args...) // cmd.Env = append(os.Environ(), "VOLTPATH="+voltpath) return cmd.CombinedOutput() } +// SuccessExit fails if any of the following conditions met: +// * out has "[WARN]" or "[ERROR]" +// * err is non-nil func SuccessExit(t *testing.T, out []byte, err error) { t.Helper() outstr := string(out) @@ -75,6 +81,9 @@ func SuccessExit(t *testing.T, out []byte, err error) { } } +// FailExit fails if any of the following conditions met: +// * out does not has "[WARN]" nor "[ERROR]" +// * err is nil func FailExit(t *testing.T, out []byte, err error) { t.Helper() outstr := string(out) @@ -86,7 +95,7 @@ func FailExit(t *testing.T, out []byte, err error) { } } -// Return sorted list of command names list +// GetCmdList returns sorted list of command names list func GetCmdList() ([]string, error) { out, err := RunVolt("help") if err != nil { @@ -117,7 +126,7 @@ func GetCmdList() ([]string, error) { return cmdList, nil } -// Set up $VOLTPATH after "volt get " +// SetUpRepos sets up $VOLTPATH after "volt get " // but the same repository is cloned only at first time // under testdata/voltpath/{testdataName}/repos/ func SetUpRepos(t *testing.T, testdataName string, rType lockjson.ReposType, reposPathList []pathutil.ReposPath, strategy string) func() { @@ -196,6 +205,8 @@ func SetUpRepos(t *testing.T, testdataName string, rType lockjson.ReposType, rep return func() {} } +// InstallConfig installs config file of "testdata/config/{filename}" +// to $VOLTPATH/config.toml func InstallConfig(t *testing.T, filename string) { configFile := filepath.Join(testdataDir, "config", filename) voltpath := os.Getenv("VOLTPATH") @@ -206,6 +217,9 @@ func InstallConfig(t *testing.T, filename string) { } } +// DefaultMatrix enumerates the combination of: +// * strategy (symlink,copy) +// * full (true,false) func DefaultMatrix(t *testing.T, f func(*testing.T, bool, string)) { for _, tt := range []struct { full bool @@ -222,6 +236,7 @@ func DefaultMatrix(t *testing.T, f func(*testing.T, bool, string)) { } } +// AvailableStrategies returns all avaiable strategies func AvailableStrategies() []string { return []string{config.SymlinkBuilder, config.CopyBuilder} } diff --git a/lockjson/lockjson.go b/lockjson/lockjson.go index 90899890..32c3da06 100644 --- a/lockjson/lockjson.go +++ b/lockjson/lockjson.go @@ -101,7 +101,7 @@ func read(doLog bool) (*LockJSON, error) { if lockJSON.Version < lockJSONVersion { if doLog { 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") + logger.Warn("If this warning persists, please run 'volt migrate lockjson'") } err = migrate(bytes, &lockJSON) if err != nil { diff --git a/lockjson/migrate.go b/lockjson/migrate.go index 0c14dcfb..56797679 100644 --- a/lockjson/migrate.go +++ b/lockjson/migrate.go @@ -35,7 +35,7 @@ func migrate1To2(rawJSON []byte, lockJSON *LockJSON) error { return err } lockJSON.CurrentProfileName = j.ActiveProfile - lockJSON.Version += 1 + lockJSON.Version++ return nil } diff --git a/main.go b/main.go index 70334e02..900a33ef 100644 --- a/main.go +++ b/main.go @@ -5,12 +5,12 @@ package main import ( "os" + "github.com/vim-volt/volt/gateway" "github.com/vim-volt/volt/logger" - "github.com/vim-volt/volt/subcmd" ) func main() { - err := subcmd.Run(os.Args, subcmd.DefaultRunner) + err := gateway.Run(os.Args, gateway.DefaultRunner) if err != nil { logger.Error(err.Msg) os.Exit(err.Code) diff --git a/pathutil/pathutil.go b/pathutil/pathutil.go index a48890cb..8108b050 100644 --- a/pathutil/pathutil.go +++ b/pathutil/pathutil.go @@ -169,9 +169,9 @@ func ConfigTOML() string { return filepath.Join(VoltPath(), "config.toml") } -// TrxLock returns fullpath of "$HOME/volt/trx.lock". -func TrxLock() string { - return filepath.Join(VoltPath(), "trx.lock") +// TrxDir returns fullpath of "$HOME/volt/trx". +func TrxDir() string { + return filepath.Join(VoltPath(), "trx") } // TempDir returns fullpath of "$HOME/tmp". diff --git a/subcmd/build.go b/subcmd/build.go index 0b871d60..30d9df17 100644 --- a/subcmd/build.go +++ b/subcmd/build.go @@ -5,7 +5,6 @@ import ( "fmt" "os" - "github.com/vim-volt/volt/logger" "github.com/vim-volt/volt/subcmd/builder" "github.com/vim-volt/volt/transaction" ) @@ -53,25 +52,27 @@ Description return fs } -func (cmd *buildCmd) Run(args []string) *Error { +func (cmd *buildCmd) Run(runctx *RunContext) (cmdErr *Error) { // Parse args fs := cmd.FlagSet() - fs.Parse(args) + fs.Parse(runctx.Args) if cmd.helped { return nil } // Begin transaction - err := transaction.Create() + trx, err := transaction.Start() if err != nil { - logger.Error() return &Error{Code: 11, Msg: "Failed to begin transaction: " + err.Error()} } - defer transaction.Remove() + defer func() { + if err := trx.Done(); err != nil { + cmdErr = &Error{Code: 13, Msg: "Failed to end transaction: " + err.Error()} + } + }() - err = builder.Build(cmd.full) + err = builder.Build(cmd.full, runctx.LockJSON, runctx.Config) if err != nil { - logger.Error() return &Error{Code: 12, Msg: "Failed to build: " + err.Error()} } diff --git a/subcmd/builder/base.go b/subcmd/builder/base.go index d0d2b87b..aec54f07 100644 --- a/subcmd/builder/base.go +++ b/subcmd/builder/base.go @@ -10,15 +10,17 @@ import ( "strings" "github.com/hashicorp/go-multierror" + "github.com/vim-volt/volt/buildinfo" "github.com/vim-volt/volt/fileutil" "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 -type BaseBuilder struct{} +type BaseBuilder struct { + lockJSON *lockjson.LockJSON +} func (builder *BaseBuilder) installVimrcAndGvimrc(profileName, vimrcPath, gvimrcPath string) error { // Save old vimrc file as {vimrc}.bak diff --git a/subcmd/builder/builder.go b/subcmd/builder/builder.go index 3730b783..80e26fc5 100644 --- a/subcmd/builder/builder.go +++ b/subcmd/builder/builder.go @@ -4,10 +4,11 @@ import ( "errors" "os" + "github.com/vim-volt/volt/buildinfo" "github.com/vim-volt/volt/config" + "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 @@ -18,15 +19,9 @@ type Builder interface { 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.New("could not read config.toml: " + err.Error()) - } - +func Build(full bool, lockJSON *lockjson.LockJSON, cfg *config.Config) error { // Get builder - blder, err := getBuilder(cfg.Build.Strategy) + blder, err := getBuilder(cfg.Build.Strategy, lockJSON) if err != nil { return err } @@ -78,12 +73,13 @@ func Build(full bool) error { return blder.Build(buildInfo, buildReposMap) } -func getBuilder(strategy string) (Builder, error) { +func getBuilder(strategy string, lockJSON *lockjson.LockJSON) (Builder, error) { + base := &BaseBuilder{lockJSON: lockJSON} switch strategy { case config.SymlinkBuilder: - return &symlinkBuilder{}, nil + return &symlinkBuilder{base}, nil case config.CopyBuilder: - return ©Builder{}, nil + return ©Builder{base}, nil default: return nil, errors.New("unknown builder type: " + strategy) } diff --git a/subcmd/builder/copy.go b/subcmd/builder/copy.go index b579f62e..4265f141 100644 --- a/subcmd/builder/copy.go +++ b/subcmd/builder/copy.go @@ -9,20 +9,20 @@ import ( "time" "github.com/hashicorp/go-multierror" + "github.com/vim-volt/volt/buildinfo" "github.com/vim-volt/volt/fileutil" "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" ) type copyBuilder struct { - BaseBuilder + *BaseBuilder } func (builder *copyBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposMap map[pathutil.ReposPath]*buildinfo.Repos) error { @@ -32,14 +32,8 @@ func (builder *copyBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposMap 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() + reposList, err := builder.lockJSON.GetCurrentReposList() if err != nil { return err } @@ -50,7 +44,7 @@ func (builder *copyBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposMap vimrcPath := filepath.Join(vimDir, pathutil.Vimrc) gvimrcPath := filepath.Join(vimDir, pathutil.Gvimrc) err = builder.installVimrcAndGvimrc( - lockJSON.CurrentProfileName, vimrcPath, gvimrcPath, + builder.lockJSON.CurrentProfileName, vimrcPath, gvimrcPath, ) if err != nil { return err @@ -98,7 +92,7 @@ func (builder *copyBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposMap } // Write bundled plugconf file - rcDir := pathutil.RCDir(lockJSON.CurrentProfileName) + rcDir := pathutil.RCDir(builder.lockJSON.CurrentProfileName) vimrc := "" if path := filepath.Join(rcDir, pathutil.ProfileVimrc); pathutil.Exists(path) { vimrc = path diff --git a/subcmd/builder/symlink.go b/subcmd/builder/symlink.go index eb94e5e5..20a35f74 100644 --- a/subcmd/builder/symlink.go +++ b/subcmd/builder/symlink.go @@ -11,16 +11,16 @@ import ( "gopkg.in/src-d/go-git.v4" + "github.com/vim-volt/volt/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 { - BaseBuilder + *BaseBuilder } // TODO: rollback when return err (!= nil) @@ -31,11 +31,7 @@ func (builder *symlinkBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposM } // Get current profile's repos list - lockJSON, err := lockjson.Read() - if err != nil { - return errors.New("could not read lock.json: " + err.Error()) - } - reposList, err := lockJSON.GetCurrentReposList() + reposList, err := builder.lockJSON.GetCurrentReposList() if err != nil { return err } @@ -46,7 +42,7 @@ func (builder *symlinkBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposM vimrcPath := filepath.Join(vimDir, pathutil.Vimrc) gvimrcPath := filepath.Join(vimDir, pathutil.Gvimrc) err = builder.installVimrcAndGvimrc( - lockJSON.CurrentProfileName, vimrcPath, gvimrcPath, + builder.lockJSON.CurrentProfileName, vimrcPath, gvimrcPath, ) if err != nil { return err @@ -86,7 +82,7 @@ func (builder *symlinkBuilder) Build(buildInfo *buildinfo.BuildInfo, buildReposM } // Write bundled plugconf file - rcDir := pathutil.RCDir(lockJSON.CurrentProfileName) + rcDir := pathutil.RCDir(builder.lockJSON.CurrentProfileName) vimrc := "" if path := filepath.Join(rcDir, pathutil.ProfileVimrc); pathutil.Exists(path) { vimrc = path diff --git a/subcmd/disable.go b/subcmd/disable.go index c3177243..a9cc2cff 100644 --- a/subcmd/disable.go +++ b/subcmd/disable.go @@ -41,8 +41,8 @@ Description return fs } -func (cmd *disableCmd) Run(args []string) *Error { - reposPathList, err := cmd.parseArgs(args) +func (cmd *disableCmd) Run(runctx *RunContext) *Error { + reposPathList, err := cmd.parseArgs(runctx.Args) if err == ErrShowedHelp { return nil } @@ -51,10 +51,11 @@ func (cmd *disableCmd) Run(args []string) *Error { } profCmd := profileCmd{} - err = profCmd.doRm(append( + runctx.Args = append( []string{"-current"}, reposPathList.Strings()..., - )) + ) + err = profCmd.doRm(runctx) if err != nil { return &Error{Code: 11, Msg: err.Error()} } diff --git a/subcmd/enable.go b/subcmd/enable.go index e51c8186..35f445a5 100644 --- a/subcmd/enable.go +++ b/subcmd/enable.go @@ -41,8 +41,8 @@ Description return fs } -func (cmd *enableCmd) Run(args []string) *Error { - reposPathList, err := cmd.parseArgs(args) +func (cmd *enableCmd) Run(runctx *RunContext) *Error { + reposPathList, err := cmd.parseArgs(runctx.Args) if err == ErrShowedHelp { return nil } @@ -51,10 +51,11 @@ func (cmd *enableCmd) Run(args []string) *Error { } profCmd := profileCmd{} - err = profCmd.doAdd(append( + runctx.Args = append( []string{"-current"}, reposPathList.Strings()..., - )) + ) + err = profCmd.doAdd(runctx) if err != nil { return &Error{Code: 11, Msg: err.Error()} } diff --git a/subcmd/get.go b/subcmd/get.go index b20796f7..597b221f 100644 --- a/subcmd/get.go +++ b/subcmd/get.go @@ -115,9 +115,9 @@ Options`) return fs } -func (cmd *getCmd) Run(args []string) *Error { +func (cmd *getCmd) Run(runctx *RunContext) *Error { // Parse args - args, err := cmd.parseArgs(args) + args, err := cmd.parseArgs(runctx.Args) if err == ErrShowedHelp { return nil } @@ -125,13 +125,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, runctx.LockJSON) if err != nil { return &Error{Code: 12, Msg: "Could not get repos list: " + err.Error()} } @@ -139,7 +133,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, runctx.LockJSON, runctx.Config) if err != nil { return &Error{Code: 20, Msg: err.Error()} } @@ -186,7 +180,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, lockJSON *lockjson.LockJSON, cfg *config.Config) (result error) { // Find matching profile profile, err := lockJSON.Profiles.FindByName(lockJSON.CurrentProfileName) if err != nil { @@ -196,17 +190,15 @@ func (cmd *getCmd) doGet(reposPathList []pathutil.ReposPath, lockJSON *lockjson. } // Begin transaction - err = transaction.Create() + trx, err := transaction.Start() if err != nil { return err } - defer transaction.Remove() - - // Read config.toml - cfg, err := config.Read() - if err != nil { - return errors.New("could not read config.toml: " + err.Error()) - } + defer func() { + if err := trx.Done(); err != nil { + result = err + } + }() done := make(chan getParallelResult, len(reposPathList)) getCount := 0 @@ -254,7 +246,7 @@ func (cmd *getCmd) doGet(reposPathList []pathutil.ReposPath, lockJSON *lockjson. } // Build ~/.vim/pack/volt dir - err = builder.Build(false) + err = builder.Build(false, lockJSON, cfg) if err != nil { return errors.New("could not build " + pathutil.VimVoltDir() + ": " + err.Error()) } diff --git a/subcmd/get_test.go b/subcmd/get_test.go index a2ee3224..cd8cba6f 100644 --- a/subcmd/get_test.go +++ b/subcmd/get_test.go @@ -586,6 +586,8 @@ func gitCommitOne(reposPath pathutil.ReposPath) (prev plumbing.Hash, current plu err = errors.New("ioutil.WriteFile() failed: " + err.Error()) return } + + // Set previous HEAD hash r, err := git.PlainOpen(reposPath.FullPath()) if err != nil { return @@ -593,9 +595,10 @@ func gitCommitOne(reposPath pathutil.ReposPath) (prev plumbing.Hash, current plu head, err := r.Head() if err != nil { return - } else { - prev = head.Hash() } + prev = head.Hash() + + // Set current HEAD hash w, err := r.Worktree() if err != nil { return @@ -617,18 +620,20 @@ func gitResetHard(reposPath pathutil.ReposPath, ref string) (current plumbing.Ha if err != nil { return } + + // Set next HEAD hash head, err := r.Head() if err != nil { return - } else { - next = head.Hash() } + next = head.Hash() + + // Set current and 'git reset --hard {current}' rev, err := r.ResolveRevision(plumbing.Revision(ref)) if err != nil { return - } else { - current = *rev } + current = *rev err = w.Reset(&git.ResetOptions{ Commit: current, Mode: git.HardReset, diff --git a/subcmd/help.go b/subcmd/help.go index 49bf679a..56884ae2 100644 --- a/subcmd/help.go +++ b/subcmd/help.go @@ -99,7 +99,8 @@ Command return fs } -func (cmd *helpCmd) Run(args []string) *Error { +func (cmd *helpCmd) Run(runctx *RunContext) *Error { + args := runctx.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) + runctx.Args = append([]string{"-help"}, args[1:]...) + fs.Run(runctx) return nil } diff --git a/subcmd/list.go b/subcmd/list.go index f3578547..dd463e9b 100644 --- a/subcmd/list.go +++ b/subcmd/list.go @@ -2,7 +2,6 @@ package subcmd import ( "encoding/json" - "errors" "flag" "fmt" "os" @@ -125,24 +124,19 @@ repos path: ` } -func (cmd *listCmd) Run(args []string) *Error { +func (cmd *listCmd) Run(runctx *RunContext) *Error { fs := cmd.FlagSet() - fs.Parse(args) + fs.Parse(runctx.Args) if cmd.helped { return nil } - if err := cmd.list(cmd.format); err != nil { + if err := cmd.list(cmd.format, runctx.LockJSON); 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.New("failed to read lock.json: " + err.Error()) - } +func (cmd *listCmd) list(format string, lockJSON *lockjson.LockJSON) error { // Parse template string t, err := template.New("volt").Funcs(cmd.funcMap(lockJSON)).Parse(format) if err != nil { diff --git a/subcmd/migrate.go b/subcmd/migrate.go index 0fe90a2c..a47f0dfb 100644 --- a/subcmd/migrate.go +++ b/subcmd/migrate.go @@ -7,7 +7,7 @@ import ( "os" "github.com/vim-volt/volt/logger" - "github.com/vim-volt/volt/subcmd/migrate" + "github.com/vim-volt/volt/subcmd/migration" ) func init() { @@ -26,7 +26,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 := migration.GetMigrater(args[0]) if err != nil { return } @@ -55,8 +55,8 @@ Available operations`) return fs } -func (cmd *migrateCmd) Run(args []string) *Error { - op, err := cmd.parseArgs(args) +func (cmd *migrateCmd) Run(runctx *RunContext) *Error { + op, err := cmd.parseArgs(runctx.Args) if err == ErrShowedHelp { return nil } @@ -64,7 +64,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(runctx.LockJSON, runctx.Config); err != nil { return &Error{Code: 11, Msg: "Failed to migrate: " + err.Error()} } @@ -72,7 +72,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) (migration.Migrater, error) { fs := cmd.FlagSet() fs.Parse(args) if cmd.helped { @@ -82,11 +82,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 migration.GetMigrater(args[0]) } func (cmd *migrateCmd) showAvailableOps(write func(string)) { - for _, m := range migrate.ListMigraters() { + for _, m := range migration.ListMigraters() { write(fmt.Sprintf(" %s", m.Name())) write(fmt.Sprintf(" %s", m.Description(true))) } diff --git a/subcmd/migrate/plugconf-config-func.go b/subcmd/migrate/plugconf-config-func.go deleted file mode 100644 index 6033c783..00000000 --- a/subcmd/migrate/plugconf-config-func.go +++ /dev/null @@ -1,105 +0,0 @@ -package migrate - -import ( - "errors" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "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" -) - -func init() { - m := &plugconfConfigMigrater{} - migrateOps[m.Name()] = m -} - -type plugconfConfigMigrater struct{} - -func (*plugconfConfigMigrater) Name() string { - return "plugconf/config-func" -} - -func (m *plugconfConfigMigrater) Description(brief bool) string { - if brief { - return "converts s:config() function name to s:on_load_pre() in all plugconf files" - } - return `Usage - volt migrate [-help] ` + m.Name() + ` - -Description - Perform migration of the function name of s:config() functions in plugconf files of all plugins. All s:config() functions are renamed to s:on_load_pre(). - "s:config()" is a old function name (see https://github.com/vim-volt/volt/issues/196). - All plugconf files are replaced with new contents.` -} - -func (*plugconfConfigMigrater) Migrate() error { - // Read lock.json - lockJSON, err := lockjson.ReadNoMigrationMsg() - if err != nil { - return errors.New("could not read lock.json: " + err.Error()) - } - - results, parseErr := plugconf.ParseMultiPlugconf(lockJSON.Repos) - if parseErr.HasErrs() { - logger.Error("Please fix the following errors before migration:") - for _, err := range parseErr.Errors().Errors { - for _, line := range strings.Split(err.Error(), "\n") { - logger.Errorf(" %s", line) - } - } - return nil - } - - type plugInfo struct { - path string - content []byte - } - infoList := make([]plugInfo, 0, len(lockJSON.Repos)) - - // Collects plugconf infomations and check errors - results.Each(func(reposPath pathutil.ReposPath, info *plugconf.ParsedInfo) { - if !info.ConvertConfigToOnLoadPreFunc() { - return // no s:config() function - } - content, err := info.GeneratePlugconf() - if err != nil { - logger.Errorf("Could not generate converted plugconf: %s", err) - return - } - infoList = append(infoList, plugInfo{ - path: reposPath.Plugconf(), - content: content, - }) - }) - - // 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) - if err != nil { - return err - } - } - - // Begin transaction - err = transaction.Create() - if err != nil { - return err - } - defer transaction.Remove() - - // Build ~/.vim/pack/volt dir - err = builder.Build(false) - if err != nil { - return errors.New("could not build " + pathutil.VimVoltDir() + ": " + err.Error()) - } - - return nil -} diff --git a/subcmd/migrate/lockjson.go b/subcmd/migration/lockjson.go similarity index 67% rename from subcmd/migrate/lockjson.go rename to subcmd/migration/lockjson.go index 0739c4a2..89c7e773 100644 --- a/subcmd/migrate/lockjson.go +++ b/subcmd/migration/lockjson.go @@ -1,10 +1,14 @@ -package migrate +package migration import ( - "errors" + "context" + "github.com/pkg/errors" + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/dsl" + "github.com/vim-volt/volt/dsl/dslctx" + "github.com/vim-volt/volt/dsl/ops" "github.com/vim-volt/volt/lockjson" - "github.com/vim-volt/volt/transaction" ) func init() { @@ -31,24 +35,12 @@ 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() +func (*lockjsonMigrater) Migrate(lockJSON *lockjson.LockJSON, cfg *config.Config) error { + ctx := dslctx.WithDSLValues(context.Background(), lockJSON, cfg) + expr, err := ops.LockJSONWriteOp.Bind() if err != nil { - return errors.New("could not read lock.json: " + err.Error()) + return errors.Wrapf(err, "cannot bind %s operator", ops.LockJSONWriteOp.String()) } - - // Begin transaction - err = transaction.Create() - if err != nil { - return err - } - defer transaction.Remove() - - // Write to lock.json - err = lockJSON.Write() - if err != nil { - return errors.New("could not write to lock.json: " + err.Error()) - } - return nil + _, err = dsl.Execute(ctx, expr) + return err } diff --git a/subcmd/migrate/migrater.go b/subcmd/migration/migrater.go similarity index 83% rename from subcmd/migrate/migrater.go rename to subcmd/migration/migrater.go index d0c0197d..f198afcb 100644 --- a/subcmd/migrate/migrater.go +++ b/subcmd/migration/migrater.go @@ -1,13 +1,16 @@ -package migrate +package migration import ( "errors" "sort" + + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/lockjson" ) // Migrater migrates many kinds of data. type Migrater interface { - Migrate() error + Migrate(*lockjson.LockJSON, *config.Config) error Name() string Description(brief bool) string } diff --git a/subcmd/migration/plugconf-config-func.go b/subcmd/migration/plugconf-config-func.go new file mode 100644 index 00000000..8c318da1 --- /dev/null +++ b/subcmd/migration/plugconf-config-func.go @@ -0,0 +1,46 @@ +package migration + +import ( + "context" + + "github.com/pkg/errors" + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/dsl" + "github.com/vim-volt/volt/dsl/dslctx" + "github.com/vim-volt/volt/dsl/ops" + "github.com/vim-volt/volt/lockjson" +) + +func init() { + m := &plugconfConfigMigrater{} + migrateOps[m.Name()] = m +} + +type plugconfConfigMigrater struct{} + +func (*plugconfConfigMigrater) Name() string { + return "plugconf/config-func" +} + +func (m *plugconfConfigMigrater) Description(brief bool) string { + if brief { + return "converts s:config() function name to s:on_load_pre() in all plugconf files" + } + return `Usage + volt migrate [-help] ` + m.Name() + ` + +Description + Perform migration of the function name of s:config() functions in plugconf files of all plugins. All s:config() functions are renamed to s:on_load_pre(). + "s:config()" is a old function name (see https://github.com/vim-volt/volt/issues/196). + All plugconf files are replaced with new contents.` +} + +func (*plugconfConfigMigrater) Migrate(lockJSON *lockjson.LockJSON, cfg *config.Config) (result error) { + ctx := dslctx.WithDSLValues(context.Background(), lockJSON, cfg) + expr, err := ops.MigratePlugconfConfigFuncOp.Bind() + if err != nil { + return errors.Wrapf(err, "cannot bind %s operator", ops.LockJSONWriteOp.String()) + } + _, err = dsl.Execute(ctx, expr) + return err +} diff --git a/subcmd/profile.go b/subcmd/profile.go index 6fb4a6c5..7e4b1809 100644 --- a/subcmd/profile.go +++ b/subcmd/profile.go @@ -99,9 +99,9 @@ Quick example return fs } -func (cmd *profileCmd) Run(args []string) *Error { +func (cmd *profileCmd) Run(runctx *RunContext) *Error { // Parse args - args, err := cmd.parseArgs(args) + args, err := cmd.parseArgs(runctx.Args) if err == ErrShowedHelp { return nil } @@ -110,23 +110,24 @@ func (cmd *profileCmd) Run(args []string) *Error { } subCmd := args[0] + runctx.Args = args[1:] switch subCmd { case "set": - err = cmd.doSet(args[1:]) + err = cmd.doSet(runctx) case "show": - err = cmd.doShow(args[1:]) + err = cmd.doShow(runctx) case "list": - err = cmd.doList(args[1:]) + err = cmd.doList(runctx) case "new": - err = cmd.doNew(args[1:]) + err = cmd.doNew(runctx) case "destroy": - err = cmd.doDestroy(args[1:]) + err = cmd.doDestroy(runctx) case "rename": - err = cmd.doRename(args[1:]) + err = cmd.doRename(runctx) case "add": - err = cmd.doAdd(args[1:]) + err = cmd.doAdd(runctx) case "rm": - err = cmd.doRm(args[1:]) + err = cmd.doRm(runctx) default: return &Error{Code: 11, Msg: "Unknown subcommand: " + subCmd} } @@ -152,15 +153,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.New("failed to read lock.json: " + err.Error()) - } - return lockJSON.CurrentProfileName, nil -} +func (cmd *profileCmd) doSet(runctx *RunContext) (result error) { + args := runctx.Args + lockJSON := runctx.LockJSON -func (cmd *profileCmd) doSet(args []string) error { // Parse args createProfile := false if len(args) > 0 && args[0] == "-n" { @@ -174,41 +170,35 @@ func (cmd *profileCmd) doSet(args []string) error { } profileName := args[0] - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.New("failed to read lock.json: " + err.Error()) - } - // Exit if current profile is same as profileName if lockJSON.CurrentProfileName == profileName { return fmt.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 { + runctx.Args = []string{profileName} + if err := cmd.doNew(runctx); err != nil { return err } - // Read lock.json again - lockJSON, err = lockjson.Read() - if err != nil { - return errors.New("failed to read lock.json: " + err.Error()) - } if _, err = lockJSON.Profiles.FindByName(profileName); err != nil { return err } } // Begin transaction - err = transaction.Create() + trx, err := transaction.Start() if err != nil { return err } - defer transaction.Remove() + defer func() { + if err := trx.Done(); err != nil { + result = err + } + }() // Set profile name lockJSON.CurrentProfileName = profileName @@ -222,7 +212,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, lockJSON, runctx.Config) if err != nil { return errors.New("could not build " + pathutil.VimVoltDir() + ": " + err.Error()) } @@ -230,19 +220,16 @@ func (cmd *profileCmd) doSet(args []string) error { return nil } -func (cmd *profileCmd) doShow(args []string) error { +func (cmd *profileCmd) doShow(runctx *RunContext) error { + args := runctx.Args + lockJSON := runctx.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.New("failed to read lock.json: " + err.Error()) - } - var profileName string if args[0] == "-current" { profileName = lockJSON.CurrentProfileName @@ -260,18 +247,21 @@ repos path: {{ . }} {{- end -}} {{- end }} -`, profileName, profileName)) +`, profileName, profileName), lockJSON) } -func (cmd *profileCmd) doList(args []string) error { +func (cmd *profileCmd) doList(runctx *RunContext) error { return (&listCmd{}).list(` {{- range .Profiles -}} {{- if eq .Name $.CurrentProfileName -}}*{{- else }} {{ end }} {{ .Name }} {{ end -}} -`) +`, runctx.LockJSON) } -func (cmd *profileCmd) doNew(args []string) error { +func (cmd *profileCmd) doNew(runctx *RunContext) (result error) { + args := runctx.Args + lockJSON := runctx.LockJSON + if len(args) == 0 { cmd.FlagSet().Usage() logger.Error("'volt profile new' receives profile name.") @@ -279,24 +269,22 @@ func (cmd *profileCmd) doNew(args []string) error { } profileName := args[0] - // Read lock.json - lockJSON, err := lockjson.Read() - if err != nil { - return errors.New("failed to read lock.json: " + err.Error()) - } - // 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") } // Begin transaction - err = transaction.Create() + trx, err := transaction.Start() if err != nil { return err } - defer transaction.Remove() + defer func() { + if err := trx.Done(); err != nil { + result = err + } + }() // Add profile lockJSON.Profiles = append(lockJSON.Profiles, lockjson.Profile{ @@ -315,25 +303,26 @@ func (cmd *profileCmd) doNew(args []string) error { return nil } -func (cmd *profileCmd) doDestroy(args []string) error { +func (cmd *profileCmd) doDestroy(runctx *RunContext) (result error) { + args := runctx.Args + lockJSON := runctx.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.New("failed to read lock.json: " + err.Error()) - } - // Begin transaction - err = transaction.Create() + trx, err := transaction.Start() if err != nil { return err } - defer transaction.Remove() + defer func() { + if err := trx.Done(); err != nil { + result = err + } + }() var merr *multierror.Error for i := range args { @@ -373,7 +362,10 @@ func (cmd *profileCmd) doDestroy(args []string) error { return merr.ErrorOrNil() } -func (cmd *profileCmd) doRename(args []string) error { +func (cmd *profileCmd) doRename(runctx *RunContext) (result error) { + args := runctx.Args + lockJSON := runctx.LockJSON + if len(args) != 2 { cmd.FlagSet().Usage() logger.Error("'volt profile rename' receives profile name.") @@ -382,12 +374,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.New("failed to read lock.json: " + err.Error()) - } - // Return error if profiles[]/name does not match oldName index := lockJSON.Profiles.FindIndexByName(oldName) if index < 0 { @@ -400,11 +386,15 @@ func (cmd *profileCmd) doRename(args []string) error { } // Begin transaction - err = transaction.Create() + trx, err := transaction.Start() if err != nil { return err } - defer transaction.Remove() + defer func() { + if err := trx.Done(); err != nil { + result = err + } + }() // Rename profile names lockJSON.Profiles[index].Name = newName @@ -432,12 +422,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.New("failed to read lock.json: " + err.Error()) - } +func (cmd *profileCmd) doAdd(runctx *RunContext) error { + args := runctx.Args + lockJSON := runctx.LockJSON // Parse args profileName, reposPathList, err := cmd.parseAddArgs(lockJSON, "add", args) @@ -466,7 +453,7 @@ func (cmd *profileCmd) doAdd(args []string) error { } // Build ~/.vim/pack/volt dir - err = builder.Build(false) + err = builder.Build(false, lockJSON, runctx.Config) if err != nil { return errors.New("could not build " + pathutil.VimVoltDir() + ": " + err.Error()) } @@ -474,12 +461,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.New("failed to read lock.json: " + err.Error()) - } +func (cmd *profileCmd) doRm(runctx *RunContext) error { + args := runctx.Args + lockJSON := runctx.LockJSON // Parse args profileName, reposPathList, err := cmd.parseAddArgs(lockJSON, "rm", args) @@ -510,7 +494,7 @@ func (cmd *profileCmd) doRm(args []string) error { } // Build ~/.vim/pack/volt dir - err = builder.Build(false) + err = builder.Build(false, lockJSON, runctx.Config) if err != nil { return errors.New("could not build " + pathutil.VimVoltDir() + ": " + err.Error()) } @@ -547,7 +531,7 @@ func (cmd *profileCmd) parseAddArgs(lockJSON *lockjson.LockJSON, subCmd string, } // Run modifyProfile and write modified structure to lock.json -func (*profileCmd) transactProfile(lockJSON *lockjson.LockJSON, profileName string, modifyProfile func(*lockjson.Profile)) (*lockjson.LockJSON, error) { +func (*profileCmd) transactProfile(lockJSON *lockjson.LockJSON, profileName string, modifyProfile func(*lockjson.Profile)) (resultLockJSON *lockjson.LockJSON, result error) { // Return error if profiles[]/name does not match profileName profile, err := lockJSON.Profiles.FindByName(profileName) if err != nil { @@ -555,11 +539,15 @@ func (*profileCmd) transactProfile(lockJSON *lockjson.LockJSON, profileName stri } // Begin transaction - err = transaction.Create() + trx, err := transaction.Start() if err != nil { return nil, err } - defer transaction.Remove() + defer func() { + if err := trx.Done(); err != nil { + result = err + } + }() modifyProfile(profile) diff --git a/subcmd/rm.go b/subcmd/rm.go index f805b2fd..9b262b59 100644 --- a/subcmd/rm.go +++ b/subcmd/rm.go @@ -62,8 +62,8 @@ Description return fs } -func (cmd *rmCmd) Run(args []string) *Error { - reposPathList, err := cmd.parseArgs(args) +func (cmd *rmCmd) Run(runctx *RunContext) *Error { + reposPathList, err := cmd.parseArgs(runctx.Args) if err == ErrShowedHelp { return nil } @@ -71,13 +71,13 @@ func (cmd *rmCmd) Run(args []string) *Error { return &Error{Code: 10, Msg: err.Error()} } - err = cmd.doRemove(reposPathList) + err = cmd.doRemove(reposPathList, runctx.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, runctx.LockJSON, runctx.Config) if err != nil { return &Error{Code: 12, Msg: "Could not build " + pathutil.VimVoltDir() + ": " + err.Error()} } @@ -108,19 +108,17 @@ 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) (result error) { // Begin transaction - err = transaction.Create() + trx, err := transaction.Start() if err != nil { return err } - defer transaction.Remove() + defer func() { + if err := trx.Done(); err != nil { + result = err + } + }() // Check if specified plugins are depended by some plugins for _, reposPath := range reposPathList { diff --git a/subcmd/self_upgrade.go b/subcmd/self_upgrade.go index be65a8fd..b35bd3ee 100644 --- a/subcmd/self_upgrade.go +++ b/subcmd/self_upgrade.go @@ -49,8 +49,8 @@ Description return fs } -func (cmd *selfUpgradeCmd) Run(args []string) *Error { - err := cmd.parseArgs(args) +func (cmd *selfUpgradeCmd) Run(runctx *RunContext) *Error { + err := cmd.parseArgs(runctx.Args) if err == ErrShowedHelp { return nil } diff --git a/subcmd/self_upgrade_test.go b/subcmd/self_upgrade_test.go index 858dd344..c53fe31d 100644 --- a/subcmd/self_upgrade_test.go +++ b/subcmd/self_upgrade_test.go @@ -5,6 +5,9 @@ import ( "os" "strings" "testing" + + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/lockjson" ) func TestVoltSelfUpgrade(t *testing.T) { @@ -31,7 +34,7 @@ func testVoltSelfUpgradeCheckFromOldVer(t *testing.T) { var err *Error out := captureOutput(t, func() { args := []string{"volt", "self-upgrade", "-check"} - err = Run(args, DefaultRunner) + runSelfUpgrade(t, args) }) if err != nil { @@ -47,7 +50,7 @@ func testVoltSelfUpgradeCheckFromCurrentVer(t *testing.T) { var err *Error out := captureOutput(t, func() { args := []string{"volt", "self-upgrade", "-check"} - err = Run(args, DefaultRunner) + runSelfUpgrade(t, args) }) if err != nil { @@ -58,6 +61,31 @@ func testVoltSelfUpgradeCheckFromCurrentVer(t *testing.T) { } } +func runSelfUpgrade(t *testing.T, args []string) { + t.Helper() + + // Read lock.json + lockJSON, err := lockjson.Read() + if err != nil { + t.Error("failed to read lock.json: " + err.Error()) + return + } + + // Read config.toml + cfg, err := config.Read() + if err != nil { + t.Error("could not read config.toml: " + err.Error()) + return + } + + cmd := &selfUpgradeCmd{} + err = cmd.Run(&RunContext{ + Args: args, + LockJSON: lockJSON, + Config: cfg, + }) +} + func captureOutput(t *testing.T, f func()) string { r, w, err := os.Pipe() if err != nil { diff --git a/subcmd/subcmd.go b/subcmd/subcmd.go new file mode 100644 index 00000000..c703b168 --- /dev/null +++ b/subcmd/subcmd.go @@ -0,0 +1,43 @@ +package subcmd + +import ( + "flag" + + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/lockjson" +) + +var cmdMap = make(map[string]Cmd) + +// LookupSubcmd looks up subcommand name +func LookupSubcmd(name string) (cmd Cmd, exists bool) { + cmd, exists = cmdMap[name] + return +} + +// Cmd represents volt's subcommand interface. +// All subcommands must implement this. +type Cmd interface { + ProhibitRootExecution(args []string) bool + Run(runctx *RunContext) *Error + FlagSet() *flag.FlagSet +} + +// RunContext is a struct to have data which are passed between gateway package +// and subcmd package +type RunContext struct { + 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/version.go b/subcmd/version.go index 5d2b0d6e..4ccf14ea 100644 --- a/subcmd/version.go +++ b/subcmd/version.go @@ -40,9 +40,9 @@ Description return fs } -func (cmd *versionCmd) Run(args []string) *Error { +func (cmd *versionCmd) Run(runctx *RunContext) *Error { fs := cmd.FlagSet() - fs.Parse(args) + fs.Parse(runctx.Args) if cmd.helped { return nil } diff --git a/transaction/transaction.go b/transaction/transaction.go index 97c9bc23..5b4a37dc 100644 --- a/transaction/transaction.go +++ b/transaction/transaction.go @@ -1,68 +1,115 @@ package transaction import ( - "errors" - "io/ioutil" "os" "path/filepath" "strconv" + "strings" + "unicode" - "github.com/vim-volt/volt/logger" + "github.com/pkg/errors" "github.com/vim-volt/volt/pathutil" ) -// Create creates $VOLTPATH/trx.lock file -func Create() error { - ownPid := []byte(strconv.Itoa(os.Getpid())) - trxLockFile := pathutil.TrxLock() - - // Create trx.lock parent directories - err := os.MkdirAll(filepath.Dir(trxLockFile), 0755) +// Start creates $VOLTPATH/trx/lock directory +func Start() (Transaction, error) { + os.MkdirAll(pathutil.TrxDir(), 0755) + lockDir := filepath.Join(pathutil.TrxDir(), "lock") + if err := os.Mkdir(lockDir, 0755); err != nil { + return nil, errors.Wrap(err, "failed to begin transaction: "+lockDir+" exists: if no other volt process is currently running, this probably means a volt process crashed earlier. Make sure no other volt process is running and remove the file manually to continue") + } + trxID, err := genNewTrxID() if err != nil { - return errors.New("failed to begin transaction: " + err.Error()) + return nil, errors.Wrap(err, "could not allocate a new transaction ID") } + return &transaction{id: trxID}, nil +} - // Return error if the file exists - if pathutil.Exists(trxLockFile) { - return errors.New("failed to begin transaction: " + pathutil.TrxLock() + " exists: if no other volt process is currently running, this probably means a volt process crashed earlier. Make sure no other volt process is running and remove the file manually to continue") - } +// Transaction provides transaction methods +type Transaction interface { + // Done renames "lock" directory to "{trxid}" directory + Done() error + + // ID returns transaction ID + ID() TrxID +} - // Write pid to trx.lock file - err = ioutil.WriteFile(trxLockFile, ownPid, 0644) +type transaction struct { + id TrxID +} + +func (trx *transaction) ID() TrxID { + return trx.id +} + +func (trx *transaction) Done() error { + lockDir := filepath.Join(pathutil.TrxDir(), "lock") + trxIDDir := filepath.Join(pathutil.TrxDir(), string(trx.id)) + return os.Rename(lockDir, trxIDDir) +} + +// genNewTrxID gets unallocated transaction ID looking $VOLTPATH/trx/ directory +func genNewTrxID() (_ TrxID, result error) { + trxDir, err := os.Open(pathutil.TrxDir()) if err != nil { - return errors.New("failed to begin transaction: " + err.Error()) + return nil, errors.Wrap(err, "could not open $VOLTPATH/trx directory") } - - // Read pid from trx.lock file - pid, err := ioutil.ReadFile(trxLockFile) + defer func() { result = trxDir.Close() }() + names, err := trxDir.Readdirnames(0) if err != nil { - return errors.New("failed to begin transaction: " + err.Error()) + return nil, errors.Wrap(err, "could not readdir of $VOLTPATH/trx directory") } - - if string(pid) != string(ownPid) { - return errors.New("transaction lock was taken by PID " + string(pid)) + var maxID TrxID + for i := range names { + if !isTrxDirName(names[i]) { + continue + } + if maxID == nil { + maxID = TrxID(names[i]) + continue + } + if greaterThan(names[i], string(maxID)) { + maxID = TrxID(names[i]) + } } - return nil + if maxID == nil { + return TrxID("1"), nil // no transaction ID directory + } + return maxID.Inc() } -// Remove removes $VOLTPATH/trx.lock file -func Remove() { - // Read pid from trx.lock file - trxLockFile := pathutil.TrxLock() - pid, err := ioutil.ReadFile(trxLockFile) - if err != nil { - logger.Error("trx.lock was already removed") - return +func greaterThan(a, b string) bool { + d := len(a) - len(b) + if d > 0 { + b = strings.Repeat("0", d) + b + } else if d < 0 { + a = strings.Repeat("0", -d) + a } + return strings.Compare(a, b) > 0 +} - // Remove trx.lock if pid is same - if string(pid) != strconv.Itoa(os.Getpid()) { - logger.Error("Cannot remove another process's trx.lock") - return +func isTrxDirName(name string) bool { + for _, r := range name { + if !unicode.IsDigit(r) { + return false + } } - err = os.Remove(trxLockFile) + return true +} + +// TrxID is a transaction ID, which is a serial number and directory name of +// transaction log file. +type TrxID []byte + +// Inc increments transaction ID +func (tid *TrxID) Inc() (TrxID, error) { + newID, err := strconv.ParseUint(string(*tid), 10, 32) if err != nil { - logger.Error("Cannot remove trx.lock: " + err.Error()) - return + return nil, err + } + if newID+uint64(1) < newID { + // TODO: compute in string? + return nil, errors.Errorf("%d + %d causes overflow", newID, 1) } + return TrxID(strconv.FormatUint(newID+uint64(1), 10)), nil } diff --git a/vendor/github.com/pkg/errors/.gitignore b/vendor/github.com/pkg/errors/.gitignore new file mode 100644 index 00000000..daf913b1 --- /dev/null +++ b/vendor/github.com/pkg/errors/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml new file mode 100644 index 00000000..588ceca1 --- /dev/null +++ b/vendor/github.com/pkg/errors/.travis.yml @@ -0,0 +1,11 @@ +language: go +go_import_path: github.com/pkg/errors +go: + - 1.4.3 + - 1.5.4 + - 1.6.2 + - 1.7.1 + - tip + +script: + - go test -v ./... diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 00000000..835ba3e7 --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md new file mode 100644 index 00000000..273db3c9 --- /dev/null +++ b/vendor/github.com/pkg/errors/README.md @@ -0,0 +1,52 @@ +# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) + +Package errors provides simple error handling primitives. + +`go get github.com/pkg/errors` + +The traditional error handling idiom in Go is roughly akin to +```go +if err != nil { + return err +} +``` +which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error. + +## Adding context to an error + +The errors.Wrap function returns a new error that adds context to the original error. For example +```go +_, err := ioutil.ReadAll(r) +if err != nil { + return errors.Wrap(err, "read failed") +} +``` +## Retrieving the cause of an error + +Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`. +```go +type causer interface { + Cause() error +} +``` +`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example: +```go +switch err := errors.Cause(err).(type) { +case *MyError: + // handle specifically +default: + // unknown error +} +``` + +[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors). + +## Contributing + +We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high. + +Before proposing a change, please discuss your change by raising an issue. + +## Licence + +BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml new file mode 100644 index 00000000..a932eade --- /dev/null +++ b/vendor/github.com/pkg/errors/appveyor.yml @@ -0,0 +1,32 @@ +version: build-{build}.{branch} + +clone_folder: C:\gopath\src\github.com\pkg\errors +shallow_clone: true # for startup speed + +environment: + GOPATH: C:\gopath + +platform: + - x64 + +# http://www.appveyor.com/docs/installed-software +install: + # some helpful output for debugging builds + - go version + - go env + # pre-installed MinGW at C:\MinGW is 32bit only + # but MSYS2 at C:\msys64 has mingw64 + - set PATH=C:\msys64\mingw64\bin;%PATH% + - gcc --version + - g++ --version + +build_script: + - go install -v ./... + +test_script: + - set PATH=C:\gopath\bin;%PATH% + - go test -v ./... + +#artifacts: +# - path: '%GOPATH%\bin\*.exe' +deploy: off diff --git a/vendor/github.com/pkg/errors/bench_test.go b/vendor/github.com/pkg/errors/bench_test.go new file mode 100644 index 00000000..0416a3cb --- /dev/null +++ b/vendor/github.com/pkg/errors/bench_test.go @@ -0,0 +1,59 @@ +// +build go1.7 + +package errors + +import ( + "fmt" + "testing" + + stderrors "errors" +) + +func noErrors(at, depth int) error { + if at >= depth { + return stderrors.New("no error") + } + return noErrors(at+1, depth) +} +func yesErrors(at, depth int) error { + if at >= depth { + return New("ye error") + } + return yesErrors(at+1, depth) +} + +func BenchmarkErrors(b *testing.B) { + var toperr error + type run struct { + stack int + std bool + } + runs := []run{ + {10, false}, + {10, true}, + {100, false}, + {100, true}, + {1000, false}, + {1000, true}, + } + for _, r := range runs { + part := "pkg/errors" + if r.std { + part = "errors" + } + name := fmt.Sprintf("%s-stack-%d", part, r.stack) + b.Run(name, func(b *testing.B) { + var err error + f := yesErrors + if r.std { + f = noErrors + } + b.ReportAllocs() + for i := 0; i < b.N; i++ { + err = f(0, r.stack) + } + b.StopTimer() + toperr = err + }) + } +} diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 00000000..842ee804 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -0,0 +1,269 @@ +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// and the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required the errors.WithStack and errors.WithMessage +// functions destructure errors.Wrap into its component operations of annotating +// an error with a stack trace and an a message, respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error which does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// causer interface is not exported by this package, but is considered a part +// of stable public API. +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported +// +// %s print the error. If the error has a Cause it will be +// printed recursively +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface. +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// Where errors.StackTrace is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d", f) +// } +// } +// +// stackTracer interface is not exported by this package, but is considered a part +// of stable public API. +// +// See the documentation for Frame.Format for more details. +package errors + +import ( + "fmt" + "io" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is call, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/pkg/errors/errors_test.go b/vendor/github.com/pkg/errors/errors_test.go new file mode 100644 index 00000000..1d8c6355 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors_test.go @@ -0,0 +1,226 @@ +package errors + +import ( + "errors" + "fmt" + "io" + "reflect" + "testing" +) + +func TestNew(t *testing.T) { + tests := []struct { + err string + want error + }{ + {"", fmt.Errorf("")}, + {"foo", fmt.Errorf("foo")}, + {"foo", New("foo")}, + {"string with format specifiers: %v", errors.New("string with format specifiers: %v")}, + } + + for _, tt := range tests { + got := New(tt.err) + if got.Error() != tt.want.Error() { + t.Errorf("New.Error(): got: %q, want %q", got, tt.want) + } + } +} + +func TestWrapNil(t *testing.T) { + got := Wrap(nil, "no error") + if got != nil { + t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWrap(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"}, + } + + for _, tt := range tests { + got := Wrap(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) + } + } +} + +type nilError struct{} + +func (nilError) Error() string { return "nil error" } + +func TestCause(t *testing.T) { + x := New("error") + tests := []struct { + err error + want error + }{{ + // nil error is nil + err: nil, + want: nil, + }, { + // explicit nil error is nil + err: (error)(nil), + want: nil, + }, { + // typed nil is nil + err: (*nilError)(nil), + want: (*nilError)(nil), + }, { + // uncaused error is unaffected + err: io.EOF, + want: io.EOF, + }, { + // caused error returns cause + err: Wrap(io.EOF, "ignored"), + want: io.EOF, + }, { + err: x, // return from errors.New + want: x, + }, { + WithMessage(nil, "whoops"), + nil, + }, { + WithMessage(io.EOF, "whoops"), + io.EOF, + }, { + WithStack(nil), + nil, + }, { + WithStack(io.EOF), + io.EOF, + }} + + for i, tt := range tests { + got := Cause(tt.err) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want) + } + } +} + +func TestWrapfNil(t *testing.T) { + got := Wrapf(nil, "no error") + if got != nil { + t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWrapf(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"}, + {Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"}, + } + + for _, tt := range tests { + got := Wrapf(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) + } + } +} + +func TestErrorf(t *testing.T) { + tests := []struct { + err error + want string + }{ + {Errorf("read error without format specifiers"), "read error without format specifiers"}, + {Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"}, + } + + for _, tt := range tests { + got := tt.err.Error() + if got != tt.want { + t.Errorf("Errorf(%v): got: %q, want %q", tt.err, got, tt.want) + } + } +} + +func TestWithStackNil(t *testing.T) { + got := WithStack(nil) + if got != nil { + t.Errorf("WithStack(nil): got %#v, expected nil", got) + } +} + +func TestWithStack(t *testing.T) { + tests := []struct { + err error + want string + }{ + {io.EOF, "EOF"}, + {WithStack(io.EOF), "EOF"}, + } + + for _, tt := range tests { + got := WithStack(tt.err).Error() + if got != tt.want { + t.Errorf("WithStack(%v): got: %v, want %v", tt.err, got, tt.want) + } + } +} + +func TestWithMessageNil(t *testing.T) { + got := WithMessage(nil, "no error") + if got != nil { + t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWithMessage(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {WithMessage(io.EOF, "read error"), "client error", "client error: read error: EOF"}, + } + + for _, tt := range tests { + got := WithMessage(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want) + } + } + +} + +// errors.New, etc values are not expected to be compared by value +// but the change in errors#27 made them incomparable. Assert that +// various kinds of errors have a functional equality operator, even +// if the result of that equality is always false. +func TestErrorEquality(t *testing.T) { + vals := []error{ + nil, + io.EOF, + errors.New("EOF"), + New("EOF"), + Errorf("EOF"), + Wrap(io.EOF, "EOF"), + Wrapf(io.EOF, "EOF%d", 2), + WithMessage(nil, "whoops"), + WithMessage(io.EOF, "whoops"), + WithStack(io.EOF), + WithStack(nil), + } + + for i := range vals { + for j := range vals { + _ = vals[i] == vals[j] // mustn't panic + } + } +} diff --git a/vendor/github.com/pkg/errors/example_test.go b/vendor/github.com/pkg/errors/example_test.go new file mode 100644 index 00000000..c1fc13e3 --- /dev/null +++ b/vendor/github.com/pkg/errors/example_test.go @@ -0,0 +1,205 @@ +package errors_test + +import ( + "fmt" + + "github.com/pkg/errors" +) + +func ExampleNew() { + err := errors.New("whoops") + fmt.Println(err) + + // Output: whoops +} + +func ExampleNew_printf() { + err := errors.New("whoops") + fmt.Printf("%+v", err) + + // Example output: + // whoops + // github.com/pkg/errors_test.ExampleNew_printf + // /home/dfc/src/github.com/pkg/errors/example_test.go:17 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/pkg/errors/_test/_testmain.go:106 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 +} + +func ExampleWithMessage() { + cause := errors.New("whoops") + err := errors.WithMessage(cause, "oh noes") + fmt.Println(err) + + // Output: oh noes: whoops +} + +func ExampleWithStack() { + cause := errors.New("whoops") + err := errors.WithStack(cause) + fmt.Println(err) + + // Output: whoops +} + +func ExampleWithStack_printf() { + cause := errors.New("whoops") + err := errors.WithStack(cause) + fmt.Printf("%+v", err) + + // Example Output: + // whoops + // github.com/pkg/errors_test.ExampleWithStack_printf + // /home/fabstu/go/src/github.com/pkg/errors/example_test.go:55 + // testing.runExample + // /usr/lib/go/src/testing/example.go:114 + // testing.RunExamples + // /usr/lib/go/src/testing/example.go:38 + // testing.(*M).Run + // /usr/lib/go/src/testing/testing.go:744 + // main.main + // github.com/pkg/errors/_test/_testmain.go:106 + // runtime.main + // /usr/lib/go/src/runtime/proc.go:183 + // runtime.goexit + // /usr/lib/go/src/runtime/asm_amd64.s:2086 + // github.com/pkg/errors_test.ExampleWithStack_printf + // /home/fabstu/go/src/github.com/pkg/errors/example_test.go:56 + // testing.runExample + // /usr/lib/go/src/testing/example.go:114 + // testing.RunExamples + // /usr/lib/go/src/testing/example.go:38 + // testing.(*M).Run + // /usr/lib/go/src/testing/testing.go:744 + // main.main + // github.com/pkg/errors/_test/_testmain.go:106 + // runtime.main + // /usr/lib/go/src/runtime/proc.go:183 + // runtime.goexit + // /usr/lib/go/src/runtime/asm_amd64.s:2086 +} + +func ExampleWrap() { + cause := errors.New("whoops") + err := errors.Wrap(cause, "oh noes") + fmt.Println(err) + + // Output: oh noes: whoops +} + +func fn() error { + e1 := errors.New("error") + e2 := errors.Wrap(e1, "inner") + e3 := errors.Wrap(e2, "middle") + return errors.Wrap(e3, "outer") +} + +func ExampleCause() { + err := fn() + fmt.Println(err) + fmt.Println(errors.Cause(err)) + + // Output: outer: middle: inner: error + // error +} + +func ExampleWrap_extended() { + err := fn() + fmt.Printf("%+v\n", err) + + // Example output: + // error + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:47 + // github.com/pkg/errors_test.ExampleCause_printf + // /home/dfc/src/github.com/pkg/errors/example_test.go:63 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/pkg/errors/_test/_testmain.go:104 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:48: inner + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:49: middle + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:50: outer +} + +func ExampleWrapf() { + cause := errors.New("whoops") + err := errors.Wrapf(cause, "oh noes #%d", 2) + fmt.Println(err) + + // Output: oh noes #2: whoops +} + +func ExampleErrorf_extended() { + err := errors.Errorf("whoops: %s", "foo") + fmt.Printf("%+v", err) + + // Example output: + // whoops: foo + // github.com/pkg/errors_test.ExampleErrorf + // /home/dfc/src/github.com/pkg/errors/example_test.go:101 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/pkg/errors/_test/_testmain.go:102 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 +} + +func Example_stackTrace() { + type stackTracer interface { + StackTrace() errors.StackTrace + } + + err, ok := errors.Cause(fn()).(stackTracer) + if !ok { + panic("oops, err does not implement stackTracer") + } + + st := err.StackTrace() + fmt.Printf("%+v", st[0:2]) // top two frames + + // Example output: + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:47 + // github.com/pkg/errors_test.Example_stackTrace + // /home/dfc/src/github.com/pkg/errors/example_test.go:127 +} + +func ExampleCause_printf() { + err := errors.Wrap(func() error { + return func() error { + return errors.Errorf("hello %s", fmt.Sprintf("world")) + }() + }(), "failed") + + fmt.Printf("%v", err) + + // Output: failed: hello world +} diff --git a/vendor/github.com/pkg/errors/format_test.go b/vendor/github.com/pkg/errors/format_test.go new file mode 100644 index 00000000..15fd7d89 --- /dev/null +++ b/vendor/github.com/pkg/errors/format_test.go @@ -0,0 +1,535 @@ +package errors + +import ( + "errors" + "fmt" + "io" + "regexp" + "strings" + "testing" +) + +func TestFormatNew(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + New("error"), + "%s", + "error", + }, { + New("error"), + "%v", + "error", + }, { + New("error"), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatNew\n" + + "\t.+/github.com/pkg/errors/format_test.go:26", + }, { + New("error"), + "%q", + `"error"`, + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatErrorf(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + Errorf("%s", "error"), + "%s", + "error", + }, { + Errorf("%s", "error"), + "%v", + "error", + }, { + Errorf("%s", "error"), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatErrorf\n" + + "\t.+/github.com/pkg/errors/format_test.go:56", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatWrap(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + Wrap(New("error"), "error2"), + "%s", + "error2: error", + }, { + Wrap(New("error"), "error2"), + "%v", + "error2: error", + }, { + Wrap(New("error"), "error2"), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatWrap\n" + + "\t.+/github.com/pkg/errors/format_test.go:82", + }, { + Wrap(io.EOF, "error"), + "%s", + "error: EOF", + }, { + Wrap(io.EOF, "error"), + "%v", + "error: EOF", + }, { + Wrap(io.EOF, "error"), + "%+v", + "EOF\n" + + "error\n" + + "github.com/pkg/errors.TestFormatWrap\n" + + "\t.+/github.com/pkg/errors/format_test.go:96", + }, { + Wrap(Wrap(io.EOF, "error1"), "error2"), + "%+v", + "EOF\n" + + "error1\n" + + "github.com/pkg/errors.TestFormatWrap\n" + + "\t.+/github.com/pkg/errors/format_test.go:103\n", + }, { + Wrap(New("error with space"), "context"), + "%q", + `"context: error with space"`, + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatWrapf(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + Wrapf(io.EOF, "error%d", 2), + "%s", + "error2: EOF", + }, { + Wrapf(io.EOF, "error%d", 2), + "%v", + "error2: EOF", + }, { + Wrapf(io.EOF, "error%d", 2), + "%+v", + "EOF\n" + + "error2\n" + + "github.com/pkg/errors.TestFormatWrapf\n" + + "\t.+/github.com/pkg/errors/format_test.go:134", + }, { + Wrapf(New("error"), "error%d", 2), + "%s", + "error2: error", + }, { + Wrapf(New("error"), "error%d", 2), + "%v", + "error2: error", + }, { + Wrapf(New("error"), "error%d", 2), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatWrapf\n" + + "\t.+/github.com/pkg/errors/format_test.go:149", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatWithStack(t *testing.T) { + tests := []struct { + error + format string + want []string + }{{ + WithStack(io.EOF), + "%s", + []string{"EOF"}, + }, { + WithStack(io.EOF), + "%v", + []string{"EOF"}, + }, { + WithStack(io.EOF), + "%+v", + []string{"EOF", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:175"}, + }, { + WithStack(New("error")), + "%s", + []string{"error"}, + }, { + WithStack(New("error")), + "%v", + []string{"error"}, + }, { + WithStack(New("error")), + "%+v", + []string{"error", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:189", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:189"}, + }, { + WithStack(WithStack(io.EOF)), + "%+v", + []string{"EOF", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:197", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:197"}, + }, { + WithStack(WithStack(Wrapf(io.EOF, "message"))), + "%+v", + []string{"EOF", + "message", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:205", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:205", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:205"}, + }, { + WithStack(Errorf("error%d", 1)), + "%+v", + []string{"error1", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:216", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:216"}, + }} + + for i, tt := range tests { + testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) + } +} + +func TestFormatWithMessage(t *testing.T) { + tests := []struct { + error + format string + want []string + }{{ + WithMessage(New("error"), "error2"), + "%s", + []string{"error2: error"}, + }, { + WithMessage(New("error"), "error2"), + "%v", + []string{"error2: error"}, + }, { + WithMessage(New("error"), "error2"), + "%+v", + []string{ + "error", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:244", + "error2"}, + }, { + WithMessage(io.EOF, "addition1"), + "%s", + []string{"addition1: EOF"}, + }, { + WithMessage(io.EOF, "addition1"), + "%v", + []string{"addition1: EOF"}, + }, { + WithMessage(io.EOF, "addition1"), + "%+v", + []string{"EOF", "addition1"}, + }, { + WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), + "%v", + []string{"addition2: addition1: EOF"}, + }, { + WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), + "%+v", + []string{"EOF", "addition1", "addition2"}, + }, { + Wrap(WithMessage(io.EOF, "error1"), "error2"), + "%+v", + []string{"EOF", "error1", "error2", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:272"}, + }, { + WithMessage(Errorf("error%d", 1), "error2"), + "%+v", + []string{"error1", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:278", + "error2"}, + }, { + WithMessage(WithStack(io.EOF), "error"), + "%+v", + []string{ + "EOF", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:285", + "error"}, + }, { + WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"), + "%+v", + []string{ + "EOF", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:293", + "inside-error", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:293", + "outside-error"}, + }} + + for i, tt := range tests { + testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) + } +} + +func TestFormatGeneric(t *testing.T) { + starts := []struct { + err error + want []string + }{ + {New("new-error"), []string{ + "new-error", + "github.com/pkg/errors.TestFormatGeneric\n" + + "\t.+/github.com/pkg/errors/format_test.go:315"}, + }, {Errorf("errorf-error"), []string{ + "errorf-error", + "github.com/pkg/errors.TestFormatGeneric\n" + + "\t.+/github.com/pkg/errors/format_test.go:319"}, + }, {errors.New("errors-new-error"), []string{ + "errors-new-error"}, + }, + } + + wrappers := []wrapper{ + { + func(err error) error { return WithMessage(err, "with-message") }, + []string{"with-message"}, + }, { + func(err error) error { return WithStack(err) }, + []string{ + "github.com/pkg/errors.(func·002|TestFormatGeneric.func2)\n\t" + + ".+/github.com/pkg/errors/format_test.go:333", + }, + }, { + func(err error) error { return Wrap(err, "wrap-error") }, + []string{ + "wrap-error", + "github.com/pkg/errors.(func·003|TestFormatGeneric.func3)\n\t" + + ".+/github.com/pkg/errors/format_test.go:339", + }, + }, { + func(err error) error { return Wrapf(err, "wrapf-error%d", 1) }, + []string{ + "wrapf-error1", + "github.com/pkg/errors.(func·004|TestFormatGeneric.func4)\n\t" + + ".+/github.com/pkg/errors/format_test.go:346", + }, + }, + } + + for s := range starts { + err := starts[s].err + want := starts[s].want + testFormatCompleteCompare(t, s, err, "%+v", want, false) + testGenericRecursive(t, err, want, wrappers, 3) + } +} + +func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { + got := fmt.Sprintf(format, arg) + gotLines := strings.SplitN(got, "\n", -1) + wantLines := strings.SplitN(want, "\n", -1) + + if len(wantLines) > len(gotLines) { + t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want) + return + } + + for i, w := range wantLines { + match, err := regexp.MatchString(w, gotLines[i]) + if err != nil { + t.Fatal(err) + } + if !match { + t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want) + } + } +} + +var stackLineR = regexp.MustCompile(`\.`) + +// parseBlocks parses input into a slice, where: +// - incase entry contains a newline, its a stacktrace +// - incase entry contains no newline, its a solo line. +// +// Detecting stack boundaries only works incase the WithStack-calls are +// to be found on the same line, thats why it is optionally here. +// +// Example use: +// +// for _, e := range blocks { +// if strings.ContainsAny(e, "\n") { +// // Match as stack +// } else { +// // Match as line +// } +// } +// +func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { + var blocks []string + + stack := "" + wasStack := false + lines := map[string]bool{} // already found lines + + for _, l := range strings.Split(input, "\n") { + isStackLine := stackLineR.MatchString(l) + + switch { + case !isStackLine && wasStack: + blocks = append(blocks, stack, l) + stack = "" + lines = map[string]bool{} + case isStackLine: + if wasStack { + // Detecting two stacks after another, possible cause lines match in + // our tests due to WithStack(WithStack(io.EOF)) on same line. + if detectStackboundaries { + if lines[l] { + if len(stack) == 0 { + return nil, errors.New("len of block must not be zero here") + } + + blocks = append(blocks, stack) + stack = l + lines = map[string]bool{l: true} + continue + } + } + + stack = stack + "\n" + l + } else { + stack = l + } + lines[l] = true + case !isStackLine && !wasStack: + blocks = append(blocks, l) + default: + return nil, errors.New("must not happen") + } + + wasStack = isStackLine + } + + // Use up stack + if stack != "" { + blocks = append(blocks, stack) + } + return blocks, nil +} + +func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) { + gotStr := fmt.Sprintf(format, arg) + + got, err := parseBlocks(gotStr, detectStackBoundaries) + if err != nil { + t.Fatal(err) + } + + if len(got) != len(want) { + t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q", + n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr) + } + + for i := range got { + if strings.ContainsAny(want[i], "\n") { + // Match as stack + match, err := regexp.MatchString(want[i], got[i]) + if err != nil { + t.Fatal(err) + } + if !match { + t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n", + n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want)) + } + } else { + // Match as message + if got[i] != want[i] { + t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i]) + } + } + } +} + +type wrapper struct { + wrap func(err error) error + want []string +} + +func prettyBlocks(blocks []string, prefix ...string) string { + var out []string + + for _, b := range blocks { + out = append(out, fmt.Sprintf("%v", b)) + } + + return " " + strings.Join(out, "\n ") +} + +func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) { + if len(beforeWant) == 0 { + panic("beforeWant must not be empty") + } + for _, w := range list { + if len(w.want) == 0 { + panic("want must not be empty") + } + + err := w.wrap(beforeErr) + + // Copy required cause append(beforeWant, ..) modified beforeWant subtly. + beforeCopy := make([]string, len(beforeWant)) + copy(beforeCopy, beforeWant) + + beforeWant := beforeCopy + last := len(beforeWant) - 1 + var want []string + + // Merge two stacks behind each other. + if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") { + want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...) + } else { + want = append(beforeWant, w.want...) + } + + testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false) + if maxDepth > 0 { + testGenericRecursive(t, err, want, list, maxDepth-1) + } + } +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 00000000..6b1f2891 --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -0,0 +1,178 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s path of source file relative to the compile time GOPATH +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + fmt.Fprintf(s, "\n%+v", f) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + fmt.Fprintf(s, "%v", []Frame(st)) + } + case 's': + fmt.Fprintf(s, "%s", []Frame(st)) + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} + +func trimGOPATH(name, file string) string { + // Here we want to get the source file path relative to the compile time + // GOPATH. As of Go 1.6.x there is no direct way to know the compiled + // GOPATH at runtime, but we can infer the number of path segments in the + // GOPATH. We note that fn.Name() returns the function name qualified by + // the import path, which does not include the GOPATH. Thus we can trim + // segments from the beginning of the file path until the number of path + // separators remaining is one more than the number of path separators in + // the function name. For example, given: + // + // GOPATH /home/user + // file /home/user/src/pkg/sub/file.go + // fn.Name() pkg/sub.Type.Method + // + // We want to produce: + // + // pkg/sub/file.go + // + // From this we can easily see that fn.Name() has one less path separator + // than our desired output. We count separators from the end of the file + // path until it finds two more than in the function name and then move + // one character forward to preserve the initial path segment without a + // leading separator. + const sep = "/" + goal := strings.Count(name, sep) + 2 + i := len(file) + for n := 0; n < goal; n++ { + i = strings.LastIndex(file[:i], sep) + if i == -1 { + // not enough separators found, set i so that the slice expression + // below leaves file unmodified + i = -len(sep) + break + } + } + // get back to 0 or trim the leading separator + file = file[i+len(sep):] + return file +} diff --git a/vendor/github.com/pkg/errors/stack_test.go b/vendor/github.com/pkg/errors/stack_test.go new file mode 100644 index 00000000..510c27a9 --- /dev/null +++ b/vendor/github.com/pkg/errors/stack_test.go @@ -0,0 +1,292 @@ +package errors + +import ( + "fmt" + "runtime" + "testing" +) + +var initpc, _, _, _ = runtime.Caller(0) + +func TestFrameLine(t *testing.T) { + var tests = []struct { + Frame + want int + }{{ + Frame(initpc), + 9, + }, { + func() Frame { + var pc, _, _, _ = runtime.Caller(0) + return Frame(pc) + }(), + 20, + }, { + func() Frame { + var pc, _, _, _ = runtime.Caller(1) + return Frame(pc) + }(), + 28, + }, { + Frame(0), // invalid PC + 0, + }} + + for _, tt := range tests { + got := tt.Frame.line() + want := tt.want + if want != got { + t.Errorf("Frame(%v): want: %v, got: %v", uintptr(tt.Frame), want, got) + } + } +} + +type X struct{} + +func (x X) val() Frame { + var pc, _, _, _ = runtime.Caller(0) + return Frame(pc) +} + +func (x *X) ptr() Frame { + var pc, _, _, _ = runtime.Caller(0) + return Frame(pc) +} + +func TestFrameFormat(t *testing.T) { + var tests = []struct { + Frame + format string + want string + }{{ + Frame(initpc), + "%s", + "stack_test.go", + }, { + Frame(initpc), + "%+s", + "github.com/pkg/errors.init\n" + + "\t.+/github.com/pkg/errors/stack_test.go", + }, { + Frame(0), + "%s", + "unknown", + }, { + Frame(0), + "%+s", + "unknown", + }, { + Frame(initpc), + "%d", + "9", + }, { + Frame(0), + "%d", + "0", + }, { + Frame(initpc), + "%n", + "init", + }, { + func() Frame { + var x X + return x.ptr() + }(), + "%n", + `\(\*X\).ptr`, + }, { + func() Frame { + var x X + return x.val() + }(), + "%n", + "X.val", + }, { + Frame(0), + "%n", + "", + }, { + Frame(initpc), + "%v", + "stack_test.go:9", + }, { + Frame(initpc), + "%+v", + "github.com/pkg/errors.init\n" + + "\t.+/github.com/pkg/errors/stack_test.go:9", + }, { + Frame(0), + "%v", + "unknown:0", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.Frame, tt.format, tt.want) + } +} + +func TestFuncname(t *testing.T) { + tests := []struct { + name, want string + }{ + {"", ""}, + {"runtime.main", "main"}, + {"github.com/pkg/errors.funcname", "funcname"}, + {"funcname", "funcname"}, + {"io.copyBuffer", "copyBuffer"}, + {"main.(*R).Write", "(*R).Write"}, + } + + for _, tt := range tests { + got := funcname(tt.name) + want := tt.want + if got != want { + t.Errorf("funcname(%q): want: %q, got %q", tt.name, want, got) + } + } +} + +func TestTrimGOPATH(t *testing.T) { + var tests = []struct { + Frame + want string + }{{ + Frame(initpc), + "github.com/pkg/errors/stack_test.go", + }} + + for i, tt := range tests { + pc := tt.Frame.pc() + fn := runtime.FuncForPC(pc) + file, _ := fn.FileLine(pc) + got := trimGOPATH(fn.Name(), file) + testFormatRegexp(t, i, got, "%s", tt.want) + } +} + +func TestStackTrace(t *testing.T) { + tests := []struct { + err error + want []string + }{{ + New("ooh"), []string{ + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:172", + }, + }, { + Wrap(New("ooh"), "ahh"), []string{ + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:177", // this is the stack of Wrap, not New + }, + }, { + Cause(Wrap(New("ooh"), "ahh")), []string{ + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:182", // this is the stack of New + }, + }, { + func() error { return New("ooh") }(), []string{ + `github.com/pkg/errors.(func·009|TestStackTrace.func1)` + + "\n\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New's caller + }, + }, { + Cause(func() error { + return func() error { + return Errorf("hello %s", fmt.Sprintf("world")) + }() + }()), []string{ + `github.com/pkg/errors.(func·010|TestStackTrace.func2.1)` + + "\n\t.+/github.com/pkg/errors/stack_test.go:196", // this is the stack of Errorf + `github.com/pkg/errors.(func·011|TestStackTrace.func2)` + + "\n\t.+/github.com/pkg/errors/stack_test.go:197", // this is the stack of Errorf's caller + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:198", // this is the stack of Errorf's caller's caller + }, + }} + for i, tt := range tests { + x, ok := tt.err.(interface { + StackTrace() StackTrace + }) + if !ok { + t.Errorf("expected %#v to implement StackTrace() StackTrace", tt.err) + continue + } + st := x.StackTrace() + for j, want := range tt.want { + testFormatRegexp(t, i, st[j], "%+v", want) + } + } +} + +func stackTrace() StackTrace { + const depth = 8 + var pcs [depth]uintptr + n := runtime.Callers(1, pcs[:]) + var st stack = pcs[0:n] + return st.StackTrace() +} + +func TestStackTraceFormat(t *testing.T) { + tests := []struct { + StackTrace + format string + want string + }{{ + nil, + "%s", + `\[\]`, + }, { + nil, + "%v", + `\[\]`, + }, { + nil, + "%+v", + "", + }, { + nil, + "%#v", + `\[\]errors.Frame\(nil\)`, + }, { + make(StackTrace, 0), + "%s", + `\[\]`, + }, { + make(StackTrace, 0), + "%v", + `\[\]`, + }, { + make(StackTrace, 0), + "%+v", + "", + }, { + make(StackTrace, 0), + "%#v", + `\[\]errors.Frame{}`, + }, { + stackTrace()[:2], + "%s", + `\[stack_test.go stack_test.go\]`, + }, { + stackTrace()[:2], + "%v", + `\[stack_test.go:225 stack_test.go:272\]`, + }, { + stackTrace()[:2], + "%+v", + "\n" + + "github.com/pkg/errors.stackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:225\n" + + "github.com/pkg/errors.TestStackTraceFormat\n" + + "\t.+/github.com/pkg/errors/stack_test.go:276", + }, { + stackTrace()[:2], + "%#v", + `\[\]errors.Frame{stack_test.go:225, stack_test.go:284}`, + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want) + } +}