From 1e338bb2cccdb425b6fb9224a638ba2da921e4c2 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 17 Mar 2019 18:32:56 +0900 Subject: [PATCH 01/30] Add a new option `--exact` to `where` command And make a `find` is shorthand for `where` --- command/find.go | 22 ---------------------- command/where.go | 28 +++++++++++++++++++--------- gogh/formatter.go | 9 +++++++++ gogh/repo.go | 2 +- main.go | 16 +++++++++++----- 5 files changed, 40 insertions(+), 37 deletions(-) delete mode 100644 command/find.go diff --git a/command/find.go b/command/find.go deleted file mode 100644 index 55d6dc4f..00000000 --- a/command/find.go +++ /dev/null @@ -1,22 +0,0 @@ -package command - -import ( - "fmt" - "log" - - "github.com/kyoh86/gogh/gogh" -) - -// Find a path of a project -func Find(ctx gogh.Context, repo *gogh.Repo) error { - log.Println("info: Finding a repository...") - path, err := gogh.FindProjectPath(ctx, repo) - if err != nil { - return err - } - if _, err := fmt.Fprintln(ctx.Stdout(), path); err != nil { - return err - } - - return nil -} diff --git a/command/where.go b/command/where.go index da32f154..fce427da 100644 --- a/command/where.go +++ b/command/where.go @@ -8,7 +8,7 @@ import ( ) // Where is a local project -func Where(ctx gogh.Context, primary bool, query string) error { +func Where(ctx gogh.Context, primary bool, exact bool, query string) error { log.Printf("info: Finding a repository by query %s", query) var walk gogh.Walker = gogh.Walk @@ -18,16 +18,26 @@ func Where(ctx gogh.Context, primary bool, query string) error { formatter := gogh.FullPathFormatter() - count := 0 - if err := gogh.Query(ctx, query, walk, func(p *gogh.Project) error { - formatter.Add(p) - count++ - return nil - }); err != nil { - return err + if exact { + repo, err := gogh.ParseRepo(query) + if err != nil { + return err + } + project, err := gogh.FindProject(ctx, repo) + if err != nil { + return err + } + formatter.Add(project) + } else { + if err := gogh.Query(ctx, query, walk, func(p *gogh.Project) error { + formatter.Add(p) + return nil + }); err != nil { + return err + } } - if count > 1 { + if formatter.Len() > 1 { log.Println("error: Multiple repositories are found") if err := formatter.PrintAll(ctx.Stderr(), "\n"); err != nil { return err diff --git a/gogh/formatter.go b/gogh/formatter.go index cb093b8e..dcd08249 100644 --- a/gogh/formatter.go +++ b/gogh/formatter.go @@ -8,6 +8,7 @@ import ( // ProjectListFormatter holds project list to print them. type ProjectListFormatter interface { Add(*Project) + Len() int PrintAll(io.Writer, string) error } @@ -87,6 +88,10 @@ func (f *shortListFormatter) Add(r *Project) { f.list = append(f.list, r) } +func (f *shortListFormatter) Len() int { + return len(f.list) +} + func (f *shortListFormatter) PrintAll(w io.Writer, sep string) error { for _, project := range f.list { if _, err := fmt.Fprint(w, f.shortName(project)+sep); err != nil { @@ -114,6 +119,10 @@ func (f *simpleCollector) Add(r *Project) { f.list = append(f.list, r) } +func (f *simpleCollector) Len() int { + return len(f.list) +} + type fullPathFormatter struct { *simpleCollector } diff --git a/gogh/repo.go b/gogh/repo.go index 467bdf7e..ce4adf41 100644 --- a/gogh/repo.go +++ b/gogh/repo.go @@ -134,7 +134,7 @@ func (r *Repo) Set(rawRepo string) error { return err } default: - return errors.New("repository remote has too many slashes") + return errors.New("repository name has too many slashes") } r.raw = raw return nil diff --git a/main.go b/main.go index 45c4768b..8926bce8 100644 --- a/main.go +++ b/main.go @@ -176,14 +176,16 @@ func create(app *kingpin.Application) (string, func() error) { func where(app *kingpin.Application) (string, func() error) { var ( primary bool + exact bool query string ) cmd := app.Command("where", "Where is a local project") cmd.Flag("primary", "Only in primary root directory").Short('p').BoolVar(&primary) + cmd.Flag("exact", "Specifies name of the project in query").Short('e').BoolVar(&exact) cmd.Arg("query", "Project name query").StringVar(&query) return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { - return command.Where(ctx, primary, query) + return command.Where(ctx, primary, exact, query) }) } @@ -218,12 +220,16 @@ func dump(app *kingpin.Application) (string, func() error) { } func find(app *kingpin.Application) (string, func() error) { - var repo gogh.Repo - cmd := app.Command("find", "Find a path of a project").Alias("search-project") - cmd.Arg("repository", "Target repository ( | / | )").Required().SetValue(&repo) + var ( + primary bool + query string + ) + cmd := app.Command("find", "Find a path of a project. This is shorthand of `gogh where --exact`") + cmd.Flag("primary", "Only in primary root directory").Short('p').BoolVar(&primary) + cmd.Arg("query", "Project name query").StringVar(&query) return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { - return command.Find(ctx, &repo) + return command.Where(ctx, primary, true, query) }) } From 9c78bd87da59cff0073595db98ee42c3df0c0eec Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 17 Mar 2019 21:15:43 +0900 Subject: [PATCH 02/30] Rename "Repo" functions to "repos" and merge command/remote to command --- command/remote/repo.go | 20 -------------------- command/repos.go | 20 ++++++++++++++++++++ gogh/remote/{repo.go => repos.go} | 4 ++-- 3 files changed, 22 insertions(+), 22 deletions(-) delete mode 100644 command/remote/repo.go create mode 100644 command/repos.go rename gogh/remote/{repo.go => repos.go} (92%) diff --git a/command/remote/repo.go b/command/remote/repo.go deleted file mode 100644 index 9e90a294..00000000 --- a/command/remote/repo.go +++ /dev/null @@ -1,20 +0,0 @@ -package remote - -import ( - "fmt" - - "github.com/kyoh86/gogh/gogh" - remo "github.com/kyoh86/gogh/gogh/remote" -) - -// Repo will show a list of repositories for a user. -func Repo(ctx gogh.Context, user string, own, collaborate, member bool, visibility, sort, direction string) error { - repos, err := remo.Repo(ctx, user, own, collaborate, member, visibility, sort, direction) - if err != nil { - return err - } - for _, repo := range repos { - fmt.Println(repo) - } - return nil -} diff --git a/command/repos.go b/command/repos.go new file mode 100644 index 00000000..bc25e1b6 --- /dev/null +++ b/command/repos.go @@ -0,0 +1,20 @@ +package command + +import ( + "fmt" + + "github.com/kyoh86/gogh/gogh" + "github.com/kyoh86/gogh/gogh/remote" +) + +// Repos will show a list of repositories for a user. +func Repos(ctx gogh.Context, user string, own, collaborate, member bool, visibility, sort, direction string) error { + repos, err := remote.Repos(ctx, user, own, collaborate, member, visibility, sort, direction) + if err != nil { + return err + } + for _, repo := range repos { + fmt.Println(repo) + } + return nil +} diff --git a/gogh/remote/repo.go b/gogh/remote/repos.go similarity index 92% rename from gogh/remote/repo.go rename to gogh/remote/repos.go index f5c89f76..1764ca97 100644 --- a/gogh/remote/repo.go +++ b/gogh/remote/repos.go @@ -7,7 +7,7 @@ import ( "github.com/kyoh86/gogh/gogh" ) -// Repo will get a list of repositories for a user. +// Repos will get a list of repositories for a user. // Parameters: // * user: Who has the repositories. Empty means the "me" (authenticated user, or GOGH_GITHUB_USER). // * own: Include repositories that are owned by the user @@ -18,7 +18,7 @@ import ( // * direction: Can be one of asc or desc default. Default means asc when using full_name, otherwise desc // Returns: // List of the url for repoisitories -func Repo(ctx gogh.Context, user string, own, collaborate, member bool, visibility, sort, direction string) ([]string, error) { +func Repos(ctx gogh.Context, user string, own, collaborate, member bool, visibility, sort, direction string) ([]string, error) { client, err := NewClient(ctx) if err != nil { return nil, err From dd3c23c6e88574dc2b8c507e83b285ad7f052e0b Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Wed, 20 Mar 2019 08:43:20 +0900 Subject: [PATCH 03/30] wip: implement configuration loader --- README.md | 5 -- command/root.go | 2 +- go.mod | 2 + go.sum | 3 + gogh/context.go | 143 ++++++++++++++++++++++------------------- gogh/context_env.go | 22 +++++-- gogh/context_test.go | 139 ++++++++++++++++++++++----------------- gogh/formatter_test.go | 34 +++++----- gogh/local.go | 6 +- gogh/local_test.go | 17 ++--- gogh/remote/repos.go | 2 +- gogh/repo.go | 12 +--- gogh/repo_test.go | 22 ++----- main.go | 80 ++++++++++++++--------- 14 files changed, 261 insertions(+), 228 deletions(-) diff --git a/README.md b/README.md index 11163326..fb7cd5d0 100644 --- a/README.md +++ b/README.md @@ -152,11 +152,6 @@ If it is not set, gogh uses `GITHUB_USER` envar or OS user name from envar (`USE The level to output logs (debug, info, warn, error or panic). Default: warn -### GOGH_GHE_HOST - -Hostnames of your GitHub Enterprise installation. -This variable can have multiple values that separated with spaces. - ### GOGH_GITHUB_TOKEN The token to connect GitHub API. diff --git a/command/root.go b/command/root.go index 7ea6f31e..b6ee5e4d 100644 --- a/command/root.go +++ b/command/root.go @@ -10,7 +10,7 @@ import ( // Root prints a gogh.root func Root(ctx gogh.Context, all bool) error { if !all { - _, err := fmt.Fprintln(ctx.Stdout(), ctx.PrimaryRoot()) + _, err := fmt.Fprintln(ctx.Stdout(), gogh.PrimaryRoot(ctx)) return err } log.Println("info: Finding all roots...") diff --git a/go.mod b/go.mod index c3d70952..cb943a9e 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,12 @@ require ( github.com/karrick/godirwalk v1.8.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/pretty v0.1.0 // indirect + github.com/kyoh86/xdg v1.0.0 github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect github.com/mitchellh/go-homedir v1.0.0 // indirect github.com/ogier/pflag v0.0.1 // indirect + github.com/pelletier/go-toml v1.2.0 github.com/pkg/errors v0.8.0 github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect diff --git a/go.sum b/go.sum index 9a53eec1..8f05ed82 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kyoh86/xdg v1.0.0 h1:TD1layQ0epNApNwGRblnQnT3S/2UH/gCQN1cmXWotvE= +github.com/kyoh86/xdg v1.0.0/go.mod h1:Z5mDqe0fxyxn3W2yTxsBAOQqIrXADQIh02wrTnaRM38= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= @@ -44,6 +46,7 @@ github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnG github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/gogh/context.go b/gogh/context.go index f3cbb1a7..e5ba2b6c 100644 --- a/gogh/context.go +++ b/gogh/context.go @@ -5,9 +5,9 @@ import ( "fmt" "go/build" "io" + "log" "os" "path/filepath" - "strings" ) // Context holds configurations and environments @@ -15,39 +15,48 @@ type Context interface { context.Context Stdout() io.Writer Stderr() io.Writer - UserName() string + LogLevel() string + + GitHubUser() string GitHubToken() string GitHubHost() string - LogLevel() string Roots() []string - PrimaryRoot() string - GHEHosts() []string } +func PrimaryRoot(ctx Context) string { + rts := ctx.Roots() + return rts[0] +} + +// Config holds configuration file values. +type Config map[string]interface{} + // CurrentContext get current context from OS envars and Git configurations -func CurrentContext(ctx context.Context) (Context, error) { - userName, err := getUserName() - if err != nil { +func CurrentContext(ctx context.Context, config Config) (Context, error) { + gitHubUser := getGitHubUser(config) + if gitHubUser == "" { + // Make the error if the GitHubUser is not set. + return nil, fmt.Errorf("failed to find user name. set %s in environment variable", envGoghGitHubUser) + } + if err := ValidateOwner(gitHubUser); err != nil { return nil, err } - gitHubToken := getGitHubToken() - gitHubHost := getGitHubHost() - logLevel := getLogLevel() - gheHosts := getGHEHosts() - roots, err := getRoots() - if err != nil { + gitHubToken := getGitHubToken(config) + gitHubHost := getGitHubHost(config) + logLevel := getLogLevel(config) + roots := getRoots(config) + if err := validateRoots(roots); err != nil { return nil, err } return &implContext{ Context: ctx, stdout: os.Stdout, stderr: os.Stderr, - userName: userName, + gitHubUser: gitHubUser, gitHubToken: gitHubToken, gitHubHost: gitHubHost, logLevel: logLevel, roots: roots, - gheHosts: gheHosts, }, nil } @@ -55,12 +64,11 @@ type implContext struct { context.Context stdout io.Writer stderr io.Writer - userName string + gitHubUser string gitHubToken string gitHubHost string logLevel string roots []string - gheHosts []string } func (c *implContext) Stdout() io.Writer { @@ -71,8 +79,8 @@ func (c *implContext) Stderr() io.Writer { return c.stderr } -func (c *implContext) UserName() string { - return c.userName +func (c *implContext) GitHubUser() string { + return c.gitHubUser } func (c *implContext) GitHubToken() string { @@ -91,64 +99,73 @@ func (c *implContext) Roots() []string { return c.roots } -func (c *implContext) PrimaryRoot() string { - rts := c.Roots() - return rts[0] -} - -func (c *implContext) GHEHosts() []string { - return c.gheHosts -} - -func getConf(envNames ...string) string { - for _, n := range envNames { - if val := os.Getenv(n); val != "" { +func getConf(def string, envar string, config Config, key string, altEnvars ...string) string { + if val := os.Getenv(envar); val != "" { + log.Printf("debug: Context %s from envar is %q", key, val) + return val + } + if config != nil { + val, ok := config[key] + if ok { + str, ok := val.(string) + if ok { + log.Printf("debug: Context %s from file is %q", key, str) + return str + } else { + log.Printf("warn: Config.%s expects string", confKeyRoot) + } + } + } + for _, e := range altEnvars { + if val := os.Getenv(e); val != "" { + log.Printf("debug: Context %s from alt-envar is %q", key, val) return val } } - return "" + return def } -func getGitHubToken() string { - return getConf(envGoghGitHubToken, envGitHubToken) +func getGitHubToken(config Config) string { + return getConf("", envGoghGitHubToken, config, confKeyGitHubToken, envGitHubToken) } -func getGitHubHost() string { - return getConf(envGoghGitHubHost, envGitHubHost) +func getGitHubHost(config Config) string { + return getConf(DefaultHost, envGoghGitHubHost, config, confKeyGitHubHost, envGitHubHost) } -func getUserName() (string, error) { - name := getConf(envGoghGitHubUser, envGitHubUser, envUserName) - if name == "" { - // Make the error if it does not match any pattern - return name, fmt.Errorf("failed to find user name. set %s in environment variable", envGoghGitHubUser) - } - if err := ValidateOwner(name); err != nil { - return name, err - } - return name, nil +func getGitHubUser(config Config) string { + return getConf("", envGoghGitHubUser, config, confKeyGitHubUser, envGitHubUser, envUserName) } -func getLogLevel() string { - if ll := os.Getenv(envLogLevel); ll != "" { - return ll - } - return "warn" // default: warn +func getLogLevel(config Config) string { + return getConf(DefaultLogLevel, envGoghLogLevel, config, confKeyLogLevel) } -func getRoots() ([]string, error) { - var roots []string - envRoot := os.Getenv(envRoot) +func getRoots(config Config) []string { + envRoot := os.Getenv(envGoghRoot) if envRoot == "" { + if config != nil { + val, ok := config[confKeyRoot] + if ok { + arr, ok := val.([]string) + if ok { + return unique(arr) + } else { + log.Printf("warn: Config.%s expects string array", confKeyRoot) + } + } + } gopaths := filepath.SplitList(build.Default.GOPATH) - roots = make([]string, 0, len(gopaths)) + roots := make([]string, 0, len(gopaths)) for _, gopath := range gopaths { roots = append(roots, filepath.Join(gopath, "src")) } - } else { - roots = filepath.SplitList(envRoot) + return unique(roots) } + return unique(filepath.SplitList(envRoot)) +} +func validateRoots(roots []string) error { for i, v := range roots { path := filepath.Clean(v) _, err := os.Stat(path) @@ -156,18 +173,14 @@ func getRoots() ([]string, error) { case err == nil: roots[i], err = filepath.EvalSymlinks(path) if err != nil { - return nil, err + return err } case os.IsNotExist(err): roots[i] = path default: - return nil, err + return err } } - return unique(roots), nil -} - -func getGHEHosts() []string { - return unique(strings.Split(os.Getenv(envGHEHosts), " ")) + return nil } diff --git a/gogh/context_env.go b/gogh/context_env.go index 01dedc4b..d77fbd3f 100644 --- a/gogh/context_env.go +++ b/gogh/context_env.go @@ -1,25 +1,35 @@ package gogh var ( - envLogLevel = "GOGH_LOG_LEVEL" + envGoghLogLevel = "GOGH_LOG_LEVEL" envGoghGitHubUser = "GOGH_GITHUB_USER" envGoghGitHubToken = "GOGH_GITHUB_TOKEN" envGoghGitHubHost = "GOGH_GITHUB_HOST" + envGoghRoot = "GOGH_ROOT" envGitHubUser = "GITHUB_USER" envGitHubToken = "GITHUB_TOKEN" envGitHubHost = "GITHUB_HOST" - envGHEHosts = "GOGH_GHE_HOST" - envRoot = "GOGH_ROOT" envNames = []string{ - envLogLevel, + envGoghLogLevel, envGoghGitHubUser, envGoghGitHubToken, envGoghGitHubHost, + envGoghRoot, envGitHubUser, envGitHubToken, envGitHubHost, envUserName, - envGHEHosts, - envRoot, } ) + +const ( + // DefaultHost is the default host of the GitHub + DefaultHost = "github.com" + DefaultLogLevel = "warn" + + confKeyLogLevel = "LogLevel" + confKeyGitHubUser = "GitHubUser" + confKeyGitHubToken = "GitHubToken" + confKeyGitHubHost = "GitHubHost" + confKeyRoot = "Root" +) diff --git a/gogh/context_test.go b/gogh/context_test.go index d4bff9fb..2097b009 100644 --- a/gogh/context_test.go +++ b/gogh/context_test.go @@ -11,103 +11,122 @@ import ( "github.com/stretchr/testify/require" ) -func TestContext(t *testing.T) { - resetEnv := func(t *testing.T) { - t.Helper() - for _, key := range envNames { - require.NoError(t, os.Setenv(key, "")) - } +func resetEnv(t *testing.T) { + t.Helper() + for _, key := range envNames { + require.NoError(t, os.Setenv(key, "")) } +} - t.Run("get context without roots", func(t *testing.T) { +func TestContext(t *testing.T) { + t.Run("get context from envar (without configuration)", func(t *testing.T) { resetEnv(t) require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) + require.NoError(t, os.Setenv(envGitHubToken, "tokenx2")) // alt: never used require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) + require.NoError(t, os.Setenv(envGitHubHost, "hostx2")) // alt: never used require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) - require.NoError(t, os.Setenv(envLogLevel, "trace")) - require.NoError(t, os.Setenv(envGHEHosts, "example.com example.com:9999")) - - ctx, err := CurrentContext(context.Background()) + require.NoError(t, os.Setenv(envGitHubUser, "kyoh87")) // alt: never used + require.NoError(t, os.Setenv(envUserName, "kyoh88")) // alt: never used + require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) + ctx, err := CurrentContext(context.Background(), nil) require.NoError(t, err) assert.Equal(t, "tokenx1", ctx.GitHubToken()) assert.Equal(t, "hostx1", ctx.GitHubHost()) - assert.Equal(t, "kyoh86", ctx.UserName()) + assert.Equal(t, "kyoh86", ctx.GitHubUser()) assert.Equal(t, "trace", ctx.LogLevel()) - assert.Equal(t, []string{"example.com", "example.com:9999"}, ctx.GHEHosts()) + assert.Equal(t, []string{"/foo", "/bar"}, ctx.Roots()) }) - t.Run("get GitHub token", func(t *testing.T) { + t.Run("get context from envar (with configuration)", func(t *testing.T) { resetEnv(t) - require.NoError(t, os.Setenv(envGitHubToken, "tokenx2")) - assert.Equal(t, "tokenx2", getGitHubToken()) + require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) + require.NoError(t, os.Setenv(envGitHubToken, "tokenx2")) // alt: never used + require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) + require.NoError(t, os.Setenv(envGitHubHost, "hostx2")) // alt: never used + require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) + require.NoError(t, os.Setenv(envGitHubUser, "kyoh87")) // alt: never used + require.NoError(t, os.Setenv(envUserName, "kyoh88")) // alt: never used + require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) + ctx, err := CurrentContext(context.Background(), Config{ // config: never used + confKeyGitHubToken: "tokenx2", + confKeyGitHubHost: "hostx2", + confKeyGitHubUser: "kyoh87", + confKeyLogLevel: "error", + confKeyRoot: []string{"/baz", "/bux"}, + }) + require.NoError(t, err) + assert.Equal(t, "tokenx1", ctx.GitHubToken()) + assert.Equal(t, "hostx1", ctx.GitHubHost()) + assert.Equal(t, "kyoh86", ctx.GitHubUser()) + assert.Equal(t, "trace", ctx.LogLevel()) + assert.Equal(t, []string{"/foo", "/bar"}, ctx.Roots()) }) - t.Run("get GitHub host", func(t *testing.T) { + t.Run("get context from configuration", func(t *testing.T) { resetEnv(t) - require.NoError(t, os.Setenv(envGitHubHost, "hostx2")) - assert.Equal(t, "hostx2", getGitHubHost()) + require.NoError(t, os.Setenv(envGitHubToken, "tokenx2")) // alt: never used + require.NoError(t, os.Setenv(envGitHubHost, "hostx2")) // alt: never used + require.NoError(t, os.Setenv(envGitHubUser, "kyoh87")) // alt: never used + require.NoError(t, os.Setenv(envUserName, "kyoh88")) // alt: never used + ctx, err := CurrentContext(context.Background(), Config{ + confKeyGitHubToken: "tokenx1", + confKeyGitHubHost: "hostx1", + confKeyGitHubUser: "kyoh86", + confKeyLogLevel: "error", + confKeyRoot: []string{"/baz", "/bux"}, + }) + require.NoError(t, err) + assert.Equal(t, "tokenx1", ctx.GitHubToken()) + assert.Equal(t, "hostx1", ctx.GitHubHost()) + assert.Equal(t, "kyoh86", ctx.GitHubUser()) + assert.Equal(t, "error", ctx.LogLevel()) + assert.Equal(t, []string{"/baz", "/bux"}, ctx.Roots()) }) - t.Run("get GitHub user name", func(t *testing.T) { + t.Run("get context from alt envars", func(t *testing.T) { resetEnv(t) - require.NoError(t, os.Setenv(envGitHubUser, "kyoh87")) - name, err := getUserName() + require.NoError(t, os.Setenv(envGitHubToken, "tokenx1")) + require.NoError(t, os.Setenv(envGitHubHost, "hostx1")) + require.NoError(t, os.Setenv(envGitHubUser, "kyoh86")) + require.NoError(t, os.Setenv(envUserName, "kyoh87")) // low priority: never used + ctx, err := CurrentContext(context.Background(), nil) require.NoError(t, err) - assert.Equal(t, "kyoh87", name) + assert.Equal(t, "tokenx1", ctx.GitHubToken()) + assert.Equal(t, "hostx1", ctx.GitHubHost()) + assert.Equal(t, "kyoh86", ctx.GitHubUser()) }) - t.Run("get OS user name", func(t *testing.T) { + t.Run("get default context", func(t *testing.T) { resetEnv(t) - require.NoError(t, os.Setenv(envUserName, "kyoh88")) - name, err := getUserName() + require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) + ctx, err := CurrentContext(context.Background(), nil) require.NoError(t, err) - assert.Equal(t, "kyoh88", name) + assert.Equal(t, "", ctx.GitHubToken()) + assert.Equal(t, DefaultHost, ctx.GitHubHost()) + assert.Equal(t, "warn", ctx.LogLevel()) + assert.Equal(t, []string{filepath.Join(build.Default.GOPATH, "src")}, ctx.Roots()) }) t.Run("expect to fail to get user name from anywhere", func(t *testing.T) { resetEnv(t) - _, err := CurrentContext(context.Background()) + _, err := CurrentContext(context.Background(), nil) require.EqualError(t, err, "failed to find user name. set GOGH_GITHUB_USER in environment variable") }) t.Run("expect to get invalid user name", func(t *testing.T) { resetEnv(t) require.NoError(t, os.Setenv(envUserName, "-kyoh88")) - _, err := getUserName() - require.NotNil(t, err) - }) - - t.Run("expect to fail to get log level from anywhere", func(t *testing.T) { - resetEnv(t) - assert.Equal(t, "warn", getLogLevel()) - }) - - t.Run("get root paths", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envRoot, "/foo:/bar")) - rts, err := getRoots() - assert.NoError(t, err) - assert.Equal(t, []string{"/foo", "/bar"}, rts) - }) - - t.Run("get root paths from GOPATH", func(t *testing.T) { - resetEnv(t) - rts, err := getRoots() - assert.NoError(t, err) - assert.Equal(t, []string{filepath.Join(build.Default.GOPATH, "src")}, rts) + _, err := CurrentContext(context.Background(), nil) + require.EqualError(t, err, "owner name may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen") }) t.Run("expects roots are not duplicated", func(t *testing.T) { resetEnv(t) - require.NoError(t, os.Setenv(envRoot, "/foo:/bar:/bar:/foo")) - rts, err := getRoots() - assert.NoError(t, err) - assert.Equal(t, []string{"/foo", "/bar"}, rts) - }) - - t.Run("expects GHE hosts are not duplicated", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGHEHosts, "example.com example.com:9999 example.com:9999 example.com")) - assert.Equal(t, []string{"example.com", "example.com:9999"}, getGHEHosts()) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar:/bar:/foo")) + assert.Equal(t, []string{"/foo", "/bar"}, getRoots(nil)) }) } diff --git a/gogh/formatter_test.go b/gogh/formatter_test.go index 39c64689..60066371 100644 --- a/gogh/formatter_test.go +++ b/gogh/formatter_test.go @@ -12,7 +12,7 @@ import ( func TestFormatter(t *testing.T) { t.Run("dry run formatters", func(t *testing.T) { - project, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/gogh") + project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/gogh") require.NoError(t, err) for _, f := range ProjectListFormats() { formatter, err := ProjectListFormat(f).Formatter() @@ -23,9 +23,9 @@ func TestFormatter(t *testing.T) { }) t.Run("rel path formatter", func(t *testing.T) { - project1, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/foo") + project1, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) - project2, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/bar") + project2, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") require.NoError(t, err) formatter, err := ProjectListFormatRelPath.Formatter() require.NoError(t, err) @@ -36,7 +36,7 @@ func TestFormatter(t *testing.T) { assert.Equal(t, `github.com/kyoh86/foo:github.com/kyoh86/bar:`, buf.String()) }) t.Run("writer error by rel path formatter", func(t *testing.T) { - project, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/foo") + project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) formatter, err := ProjectListFormatRelPath.Formatter() require.NoError(t, err) @@ -45,9 +45,9 @@ func TestFormatter(t *testing.T) { }) t.Run("full path formatter", func(t *testing.T) { - project1, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/foo") + project1, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) - project2, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/bar") + project2, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") require.NoError(t, err) formatter, err := ProjectListFormatFullPath.Formatter() require.NoError(t, err) @@ -58,7 +58,7 @@ func TestFormatter(t *testing.T) { assert.Equal(t, `/go/src/github.com/kyoh86/foo:/go/src/github.com/kyoh86/bar:`, buf.String()) }) t.Run("writer error by full path formatter", func(t *testing.T) { - project, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/foo") + project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) formatter, err := ProjectListFormatFullPath.Formatter() require.NoError(t, err) @@ -67,9 +67,9 @@ func TestFormatter(t *testing.T) { }) t.Run("url formatter", func(t *testing.T) { - project1, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/foo") + project1, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) - project2, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/bar") + project2, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") require.NoError(t, err) formatter, err := ProjectListFormatURL.Formatter() require.NoError(t, err) @@ -80,7 +80,7 @@ func TestFormatter(t *testing.T) { assert.Equal(t, `https://github.com/kyoh86/foo:https://github.com/kyoh86/bar:`, buf.String()) }) t.Run("writer error by url formatter", func(t *testing.T) { - project, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/foo") + project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) formatter, err := ProjectListFormatURL.Formatter() require.NoError(t, err) @@ -89,17 +89,17 @@ func TestFormatter(t *testing.T) { }) t.Run("short formatter", func(t *testing.T) { - project1, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/foo") + project1, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) - project2, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/bar") + project2, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") require.NoError(t, err) - project3, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh87/bar") + project3, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh87/bar") require.NoError(t, err) - project4, err := parseProject(&implContext{gheHosts: []string{"example.com"}}, "/go/src", "/go/src/example.com/kyoh86/bar") + project4, err := parseProject(&implContext{gitHubHost: "example.com"}, "/go/src", "/go/src/example.com/kyoh86/bar") require.NoError(t, err) - project5, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/baz") + project5, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/baz") require.NoError(t, err) - project6, err := parseProject(&implContext{}, "/foo", "/foo/github.com/kyoh86/baz") + project6, err := parseProject(&implContext{gitHubHost: "github.com"}, "/foo", "/foo/github.com/kyoh86/baz") require.NoError(t, err) formatter, err := ProjectListFormatShort.Formatter() require.NoError(t, err) @@ -114,7 +114,7 @@ func TestFormatter(t *testing.T) { assert.Equal(t, `foo:github.com/kyoh86/bar:kyoh87/bar:example.com/kyoh86/bar:/go/src/github.com/kyoh86/baz:/foo/github.com/kyoh86/baz:`, buf.String()) }) t.Run("writer error by short formatter", func(t *testing.T) { - project, err := parseProject(&implContext{}, "/go/src", "/go/src/github.com/kyoh86/foo") + project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) formatter, err := ProjectListFormatShort.Formatter() require.NoError(t, err) diff --git a/gogh/local.go b/gogh/local.go index 107f7e79..63bb8378 100644 --- a/gogh/local.go +++ b/gogh/local.go @@ -70,7 +70,7 @@ func NewProject(ctx Context, repo *Repo) (*Project, error) { return nil, err } relPath := repo.RelPath(ctx) - fullPath := filepath.Join(ctx.PrimaryRoot(), relPath) + fullPath := filepath.Join(PrimaryRoot(ctx), relPath) return &Project{ FullPath: fullPath, RelPath: relPath, @@ -102,7 +102,7 @@ func (p *Project) Subpaths() []string { // IsInPrimaryRoot check which the repository is in primary root directory for gogh func (p *Project) IsInPrimaryRoot(ctx Context) bool { - return strings.HasPrefix(p.FullPath, ctx.PrimaryRoot()) + return strings.HasPrefix(p.FullPath, PrimaryRoot(ctx)) } func isVcsDir(path string) bool { @@ -180,7 +180,7 @@ func parseProject(ctx Context, root string, fullPath string) (*Project, error) { // WalkInPrimary thorugh projects (local repositories) in the first gogh.root directory func WalkInPrimary(ctx Context, callback WalkFunc) error { - return walkInPath(ctx, ctx.PrimaryRoot(), callback) + return walkInPath(ctx, PrimaryRoot(ctx), callback) } // Walk thorugh projects (local repositories) in gogh.root directories diff --git a/gogh/local_test.go b/gogh/local_test.go index 733b7ec1..34f4d997 100644 --- a/gogh/local_test.go +++ b/gogh/local_test.go @@ -14,7 +14,7 @@ import ( func TestParseProject(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - ctx := implContext{roots: []string{tmp}} + ctx := implContext{roots: []string{tmp}, gitHubHost: "github.com"} t.Run("in primary root", func(t *testing.T) { path := filepath.Join(tmp, "github.com", "kyoh86", "gogh") @@ -67,7 +67,7 @@ func TestParseProject(t *testing.T) { func TestFindOrNewProject(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - ctx := implContext{roots: []string{tmp}, userName: "kyoh86"} + ctx := implContext{roots: []string{tmp}, gitHubUser: "kyoh86", gitHubHost: "github.com"} path := filepath.Join(tmp, "github.com", "kyoh86", "gogh") @@ -189,7 +189,7 @@ func TestWalk(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(path, ".git"), 0755)) require.NoError(t, ioutil.WriteFile(filepath.Join(tmp, "foo"), nil, 0644)) - ctx := implContext{roots: []string{tmp, filepath.Join(tmp, "foo")}} + ctx := implContext{roots: []string{tmp, filepath.Join(tmp, "foo")}, gitHubHost: "github.com"} err = errors.New("sample error") assert.EqualError(t, Walk(&ctx, func(p *Project) error { assert.Equal(t, path, p.FullPath) @@ -206,7 +206,7 @@ func TestList_Symlink(t *testing.T) { symDir, err := ioutil.TempDir("", "") require.NoError(t, err) - ctx := &implContext{roots: []string{root}} + ctx := &implContext{roots: []string{root}, gitHubHost: "github.com"} err = os.MkdirAll(filepath.Join(root, "github.com", "atom", "atom", ".git"), 0777) require.NoError(t, err) @@ -237,12 +237,10 @@ func TestQuery(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(path2, ".git"), 0755)) path3 := filepath.Join(root1, "github.com", "kyoh86", "foo") require.NoError(t, os.MkdirAll(filepath.Join(path3, ".git"), 0755)) - path4 := filepath.Join(root1, "example.com", "kyoh86", "gogh") - require.NoError(t, os.MkdirAll(filepath.Join(path4, ".git"), 0755)) path5 := filepath.Join(root2, "github.com", "kyoh86", "gogh") require.NoError(t, os.MkdirAll(filepath.Join(path5, ".git"), 0755)) - ctx := implContext{roots: []string{root1, root2}, gheHosts: []string{"example.com"}} + ctx := implContext{roots: []string{root1, root2}, gitHubHost: "github.com"} assert.NoError(t, Query(&ctx, "never found", Walk, func(*Project) error { t.Fatal("should not be called but...") @@ -253,7 +251,6 @@ func TestQuery(t *testing.T) { expect := map[string]struct{}{ path1: {}, path2: {}, - path4: {}, path5: {}, } assert.NoError(t, Query(&ctx, "gogh", Walk, func(p *Project) error { @@ -267,7 +264,6 @@ func TestQuery(t *testing.T) { expect := map[string]struct{}{ path1: {}, path2: {}, - path4: {}, path5: {}, } assert.NoError(t, Query(&ctx, "gog", Walk, func(p *Project) error { @@ -280,7 +276,6 @@ func TestQuery(t *testing.T) { t.Run("OwnerAndName", func(t *testing.T) { expect := map[string]struct{}{ path1: {}, - path4: {}, path5: {}, } assert.NoError(t, Query(&ctx, "kyoh86/gogh", Walk, func(p *Project) error { @@ -293,7 +288,6 @@ func TestQuery(t *testing.T) { t.Run("PartialOwnerAndName", func(t *testing.T) { expect := map[string]struct{}{ path1: {}, - path4: {}, path5: {}, } assert.NoError(t, Query(&ctx, "yoh86/gog", Walk, func(p *Project) error { @@ -331,7 +325,6 @@ func TestQuery(t *testing.T) { expect := map[string]struct{}{ path1: {}, path2: {}, - path4: {}, } assert.NoError(t, Query(&ctx, "gogh", WalkInPrimary, func(p *Project) error { assert.Contains(t, expect, p.FullPath) diff --git a/gogh/remote/repos.go b/gogh/remote/repos.go index 1764ca97..1277d24b 100644 --- a/gogh/remote/repos.go +++ b/gogh/remote/repos.go @@ -44,7 +44,7 @@ func Repos(ctx gogh.Context, user string, own, collaborate, member bool, visibil } // If the context has no authentication token, specifies context user name for "me". if user == "" && !authenticated(ctx) { - user = ctx.UserName() + user = ctx.GitHubUser() } opts := &github.RepositoryListOptions{ diff --git a/gogh/repo.go b/gogh/repo.go index ce4adf41..a57cac8a 100644 --- a/gogh/repo.go +++ b/gogh/repo.go @@ -42,14 +42,9 @@ func CheckRepoHost(ctx Context, repo *Repo) error { // ValidateHost that repo is in supported host func ValidateHost(ctx Context, host string) error { - if host == DefaultHost { + if host == ctx.GitHubHost() { return nil } - for _, h := range ctx.GHEHosts() { - if h == host { - return nil - } - } return fmt.Errorf("not supported host: %q", host) } @@ -140,9 +135,6 @@ func (r *Repo) Set(rawRepo string) error { return nil } -// DefaultHost is the default host of the GitHub -const DefaultHost = "github.com" - // Scheme returns scheme of the repository func (r *Repo) Scheme(_ Context) string { return r.scheme @@ -156,7 +148,7 @@ func (r *Repo) Host(_ Context) string { // Owner returns a user name of an owner of the repository func (r *Repo) Owner(ctx Context) string { if r.owner == "" { - return ctx.UserName() + return ctx.GitHubUser() } return r.owner } diff --git a/gogh/repo_test.go b/gogh/repo_test.go index 97348440..4f53a7c5 100644 --- a/gogh/repo_test.go +++ b/gogh/repo_test.go @@ -55,7 +55,7 @@ func TestRepo(t *testing.T) { }) t.Run("name only spec", func(t *testing.T) { - ctx := &implContext{userName: "kyoh86"} + ctx := &implContext{gitHubUser: "kyoh86"} spec, err := ParseRepo("gogh") require.NoError(t, err) assert.Equal(t, "kyoh86/gogh", spec.FullName(ctx)) @@ -146,29 +146,15 @@ func TestRepos(t *testing.T) { func TestCheckRepoHost(t *testing.T) { t.Run("valid GitHub URL", func(t *testing.T) { - ctx := implContext{} + ctx := implContext{gitHubHost: "github.com"} assert.NoError(t, CheckRepoHost(&ctx, parseURL(t, "https://github.com/kyoh86/gogh"))) }) - t.Run("valid GHE URL", func(t *testing.T) { - ctx := implContext{ - gheHosts: []string{"example.com"}, - } - assert.NoError(t, CheckRepoHost(&ctx, parseURL(t, "https://example.com/kyoh86/gogh"))) - }) t.Run("valid GitHub URL with trailing slashes", func(t *testing.T) { - ctx := implContext{} + ctx := implContext{gitHubHost: "github.com"} assert.NoError(t, CheckRepoHost(&ctx, parseURL(t, "https://github.com/kyoh86/gogh/"))) }) - t.Run("valid GHE URL with trailing slashes", func(t *testing.T) { - ctx := implContext{ - gheHosts: []string{"example.com"}, - } - assert.NoError(t, CheckRepoHost(&ctx, parseURL(t, "https://example.com/kyoh86/gogh/"))) - }) t.Run("not supported host URL", func(t *testing.T) { - ctx := implContext{ - gheHosts: []string{"example.com"}, - } + ctx := implContext{gitHubHost: "github.com"} assert.EqualError(t, CheckRepoHost(&ctx, parseURL(t, "https://kyoh86.work/kyoh86/gogh")), `not supported host: "kyoh86.work"`) }) } diff --git a/main.go b/main.go index 8926bce8..4c47ff72 100644 --- a/main.go +++ b/main.go @@ -6,12 +6,14 @@ import ( "log" "net/url" "os" + "path/filepath" "github.com/alecthomas/kingpin" "github.com/comail/colog" "github.com/kyoh86/gogh/command" - "github.com/kyoh86/gogh/command/remote" "github.com/kyoh86/gogh/gogh" + "github.com/kyoh86/xdg" + toml "github.com/pelletier/go-toml" ) var ( @@ -21,10 +23,28 @@ var ( ) func main() { + var configFile string + var config gogh.Config + app := kingpin.New("gogh", "GO GitHub project manager").Version(fmt.Sprintf("%s-%s (%s)", version, commit, date)).Author("kyoh86") + app.Flag("config", "configuration file"). + Default(filepath.Join(xdg.CacheHome(), "gogh", "config.yml")). + Envar("GOGH_CONFIG"). + Action(func(ctx *kingpin.ParseContext) error { + file, err := os.Open(configFile) + switch { + case err == nil: + // noop + case os.IsNotExist(err): + return nil + default: + return err + } + return toml.NewDecoder(file).Decode(&config) + }).StringVar(&configFile) cmds := map[string]func() error{} - for _, f := range []func(*kingpin.Application) (string, func() error){ + for _, f := range []func(*kingpin.Application, gogh.Config) (string, func() error){ get, bulk, pipe, @@ -39,7 +59,7 @@ func main() { repos, } { - key, run := f(app) + key, run := f(app, config) cmds[key] = run } if err := cmds[kingpin.MustParse(app.Parse(os.Args[1:]))](); err != nil { @@ -47,9 +67,9 @@ func main() { } } -func wrapContext(f func(gogh.Context) error) func() error { +func wrapContext(config gogh.Config, f func(gogh.Context) error) func() error { return func() error { - ctx, err := gogh.CurrentContext(context.Background()) + ctx, err := gogh.CurrentContext(context.Background(), config) if err != nil { return err } @@ -65,7 +85,7 @@ func wrapContext(f func(gogh.Context) error) func() error { } } -func get(app *kingpin.Application) (string, func() error) { +func get(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( update bool withSSH bool @@ -78,12 +98,12 @@ func get(app *kingpin.Application) (string, func() error) { cmd.Flag("shallow", "Do a shallow clone").BoolVar(&shallow) cmd.Arg("repositories", "Target repositories ( | / | )").Required().SetValue(&repoNames) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.GetAll(ctx, update, withSSH, shallow, repoNames) }) } -func bulk(app *kingpin.Application) (string, func() error) { +func bulk(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( update bool withSSH bool @@ -94,12 +114,12 @@ func bulk(app *kingpin.Application) (string, func() error) { cmd.Flag("ssh", "Clone with SSH").BoolVar(&withSSH) cmd.Flag("shallow", "Do a shallow clone").BoolVar(&shallow) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.Bulk(ctx, update, withSSH, shallow) }) } -func pipe(app *kingpin.Application) (string, func() error) { +func pipe(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( update bool withSSH bool @@ -114,12 +134,12 @@ func pipe(app *kingpin.Application) (string, func() error) { cmd.Arg("command", "Subcommand calling to get import paths").StringVar(&srcCmd) cmd.Arg("command-args", "Arguments that will be passed to subcommand").StringsVar(&srcCmdArgs) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.Pipe(ctx, update, withSSH, shallow, srcCmd, srcCmdArgs) }) } -func fork(app *kingpin.Application) (string, func() error) { +func fork(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( update bool withSSH bool @@ -138,12 +158,12 @@ func fork(app *kingpin.Application) (string, func() error) { cmd.Flag("org", "Fork the repository within this organization").PlaceHolder("ORGANIZATION").StringVar(&organization) cmd.Arg("repository", "Target repository ( | / | )").Required().SetValue(&repo) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.Fork(ctx, update, withSSH, shallow, noRemote, remoteName, organization, &repo) }) } -func create(app *kingpin.Application) (string, func() error) { +func create(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( private bool description string @@ -168,12 +188,12 @@ func create(app *kingpin.Application) (string, func() error) { cmd.Flag("shared", "Specify that the Git repository is to be shared amongst several users.").SetValue(&shared) cmd.Arg("repository", "Target repository ( | / | )").Required().SetValue(&repo) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.New(ctx, private, description, homepage, browse, clip, bare, template, separateGitDir, shared, &repo) }) } -func where(app *kingpin.Application) (string, func() error) { +func where(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( primary bool exact bool @@ -184,12 +204,12 @@ func where(app *kingpin.Application) (string, func() error) { cmd.Flag("exact", "Specifies name of the project in query").Short('e').BoolVar(&exact) cmd.Arg("query", "Project name query").StringVar(&query) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.Where(ctx, primary, exact, query) }) } -func list(app *kingpin.Application) (string, func() error) { +func list(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( format string primary bool @@ -200,12 +220,12 @@ func list(app *kingpin.Application) (string, func() error) { cmd.Flag("primary", "Only in primary root directory").Short('p').BoolVar(&primary) cmd.Arg("query", "Project name query").StringVar(&query) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.List(ctx, gogh.ProjectListFormat(format), primary, query) }) } -func dump(app *kingpin.Application) (string, func() error) { +func dump(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( primary bool query string @@ -214,12 +234,12 @@ func dump(app *kingpin.Application) (string, func() error) { cmd.Flag("primary", "Only in primary root directory").Short('p').BoolVar(&primary) cmd.Arg("query", "Project name query").StringVar(&query) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.List(ctx, gogh.ProjectListFormatURL, primary, query) }) } -func find(app *kingpin.Application) (string, func() error) { +func find(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( primary bool query string @@ -228,22 +248,22 @@ func find(app *kingpin.Application) (string, func() error) { cmd.Flag("primary", "Only in primary root directory").Short('p').BoolVar(&primary) cmd.Arg("query", "Project name query").StringVar(&query) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.Where(ctx, primary, true, query) }) } -func root(app *kingpin.Application) (string, func() error) { +func root(app *kingpin.Application, config gogh.Config) (string, func() error) { var all bool cmd := app.Command("root", "Show repositories' root") cmd.Flag("all", "Show all roots").Envar("GOGH_FLAG_ROOT_ALL").BoolVar(&all) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.Root(ctx, all) }) } -func setup(app *kingpin.Application) (string, func() error) { +func setup(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( cdFuncName string shell string @@ -252,12 +272,12 @@ func setup(app *kingpin.Application) (string, func() error) { cmd.Flag("cd-function-name", "Name of the function to define").Default("gogogh").Hidden().StringVar(&cdFuncName) cmd.Flag("shell", "Target shell path").Envar("SHELL").Hidden().StringVar(&shell) - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { return command.Setup(ctx, cdFuncName, shell) }) } -func repos(app *kingpin.Application) (string, func() error) { +func repos(app *kingpin.Application, config gogh.Config) (string, func() error) { var ( user string own bool @@ -276,7 +296,7 @@ func repos(app *kingpin.Application) (string, func() error) { cmd.Flag("sort", "Sort repositories by").Default("full_name").EnumVar(&sort, "created", "updated", "pushed", "full_name") cmd.Flag("direction", "Sort direction").Default("default").EnumVar(&direction, "asc", "desc", "default") - return cmd.FullCommand(), wrapContext(func(ctx gogh.Context) error { - return remote.Repo(ctx, user, own, collaborate, member, visibility, sort, direction) + return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return command.Repos(ctx, user, own, collaborate, member, visibility, sort, direction) }) } From 26d168f42468f210ae31df56a20692235d660ff1 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Tue, 26 Mar 2019 01:04:42 +0900 Subject: [PATCH 04/30] Load options from configuration file instead of environment variable --- README.md | 96 ++++++++++------- command/config.go | 194 ++++++++++++++++++++++++++++++++++ command/root.go | 4 +- {gogh => command}/util.go | 2 +- go.mod | 4 + go.sum | 12 +++ gogh/context.go | 140 ++---------------------- gogh/context_env.go | 35 ------ gogh/context_env_others.go | 7 -- gogh/context_env_windows.go | 3 - gogh/context_test.go | 132 ----------------------- gogh/local.go | 8 +- gogh/local_test.go | 22 ++-- gogh/remote/client.go | 2 +- gogh/repo.go | 7 +- gogh/repo_test.go | 4 +- internal/mainutil/mainutil.go | 50 +++++++++ main.go | 134 +++++++++++++---------- 18 files changed, 429 insertions(+), 427 deletions(-) create mode 100644 command/config.go rename {gogh => command}/util.go (93%) delete mode 100644 gogh/context_env.go delete mode 100644 gogh/context_env_others.go delete mode 100644 gogh/context_env_windows.go delete mode 100644 gogh/context_test.go create mode 100644 internal/mainutil/mainutil.go diff --git a/README.md b/README.md index fb7cd5d0..6e27a046 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,17 @@ You can also list projects (local repositories) (`gogh list`), find a project (` ## SYNOPSIS ``` -gogh get [--update,u] [--ssh] [--shallow] [( | / | )...] -gogh bulk-get [--update,u] [--ssh] [--shallow] -gogh pipe-get [--update,u] [--ssh] [--shallow] ... -gogh fork [--update,u] [--ssh] [--shallow] [--no-remote] [--remote-name=] [--org= | / | ) -gogh new [--update,u] [--ssh] [--shallow] [--no-remote] [--remote-name=] [--org= | / | ) -gogh list [--format,f=short|full|relative|url] [--primary,p] [] -gogh dump [--primary,p] [] -gogh find () -gogh where [--primary,p] [] -gogh repo [--user=] [--own] [--collaborate] [--member] [--visibility=] [--sort=] [--direction=] -gogh root [--all] +gogh [--config=] get [--update,u] [--ssh] [--shallow] [( | / | )...] +gogh [--config=] bulk-get [--update,u] [--ssh] [--shallow] +gogh [--config=] pipe-get [--update,u] [--ssh] [--shallow] ... +gogh [--config=] fork [--update,u] [--ssh] [--shallow] [--no-remote] [--remote-name=] [--org= | / | ) +gogh [--config=] new [--update,u] [--ssh] [--shallow] [--no-remote] [--remote-name=] [--org= | / | ) +gogh [--config=] list [--format,f=short|full|relative|url] [--primary,p] [] +gogh [--config=] dump [--primary,p] [] +gogh [--config=] find () +gogh [--config=] where [--primary,p] [] +gogh [--config=] repo [--user=] [--own] [--collaborate] [--member] [--visibility=] [--sort=] [--direction=] +gogh [--config=] root [--all] ``` ## INSTALLATION @@ -55,6 +55,49 @@ brew update brew install gogh ``` +## CONFIGURATIONS + +It's possible to change targets by a preference **TOML file**. +If you don't set `--config` flag or `GOGH_CONFIG` environment variable, +`gogh` loads configurations from `${XDG_CONFIG_HOME:-$HOME/.config}/gogh/config.toml` + +Each of propoerties are able to be overwritten by environment variables. + +### (REQUIRED) GitHubUser + +A name of your GitHub user (i.e. `kyoh86`). + +If an environment variable `GOGH_GITHUB_USER` is set, its value is used instead. + +### Root + +The paths to directory under which cloned repositories are placed. +See [DIRECTORY STRUCTURES](#DIRECTORY+STRUCTURES) below. Default: `~/go/src`. + +This property can have multiple values. +If so, the first one becomes primary one i.e. new repository clones are always created under it. +You may want to specify `$GOPATH/src` as a secondary root. + +If an environment variable `GOGH_ROOT` is set, its value is used instead. + +### LogLevel + +The level to output logs (debug, info, warn, error or panic). Default: `warn`. + +If an environment variable `GOGH_LOG_LEVEL` is set, its value is used instead. + +### GitHubToken + +The token to connect GitHub API. + +If an environment variable `GOGH_GITHUB_TOKEN` is set, its value is used instead. + +### GitHubHost + +The host name to connect to GitHub. Default: `github.com`. + +If an environment variable `GOGH_GITHUB_HOST` is set, its value is used instead. + ## COMMANDS ``` @@ -134,37 +177,16 @@ Prints repositories' root (i.e. `gogh.root`). Without `--all` option, the primar ## ENVIRONMENT VARIABLES -### GOGH_ROOT - -The paths to directory under which cloned repositories are placed. -See [DIRECTORY STRUCTURES](#DIRECTORY+STRUCTURES) below. Defaults to `~/go/src`. - -This variable can have multiple values. -If so, the first one becomes primary one i.e. new repository clones are always created under it. -You may want to specify `$GOPATH/src` as a secondary root (environment variables should be expanded.) - -### GOGH_GITHUB_USER - -A name of your GitHub user (i.e. `kyoh86`). -If it is not set, gogh uses `GITHUB_USER` envar or OS user name from envar (`USERNAME` in windows, `USER` in others) instead. - -### GOGH_LOG_LEVEL - -The level to output logs (debug, info, warn, error or panic). Default: warn - -### GOGH_GITHUB_TOKEN - -The token to connect GitHub API. -If it is not set, gogh uses `GITHUB_TOKEN` envar instead. +Some environment variables are used for flags. -### GOGH_GITHUB_HOST +### GOGH_CONFIG -The host to connect GitHub on default. -If it is not set, gogh uses `GITHUB_HOST` envar or `github.com` instead. +You can set it instead of `--config` flag (configuration file path). +Default: `${XDG_CONFIG_HOME:-$HOME/.config}/gogh/config.toml`. ### GOGH_FLAG_ROOT_ALL -We can set it truely value and `gogh root` shows all of the roots like `gogh root --all`. +You can set it truely value and `gogh root` shows all of the roots like `gogh root --all`. If we want show only primary root, call `gogh root --no-all`. e.g. diff --git a/command/config.go b/command/config.go new file mode 100644 index 00000000..47e37adf --- /dev/null +++ b/command/config.go @@ -0,0 +1,194 @@ +package command + +import ( + "context" + "go/build" + "io" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/joeshaw/envdecode" + "github.com/kyoh86/gogh/gogh" + "github.com/pelletier/go-toml" +) + +// Config holds configuration file values. +type Config struct { + context.Context + VLogLevel string `toml:"loglevel,omitempty" env:"GOGH_LOG_LEVEL"` + VRoot RootConfig `toml:"root,omitempty" env:"GOGH_ROOT" envSeparator:":"` + GitHub GitHubConfig `toml:"github,omitempty"` +} + +func (c *Config) Stdout() io.Writer { + return os.Stdout +} + +func (c *Config) Stderr() io.Writer { + return os.Stderr +} + +func (c *Config) GitHubUser() string { + return c.GitHub.User +} + +func (c *Config) GitHubToken() string { + return c.GitHub.Token +} + +func (c *Config) GitHubHost() string { + return c.GitHub.Host +} + +func (c *Config) LogLevel() string { + return c.VLogLevel +} + +func (c *Config) Root() []string { + return c.VRoot +} + +func (c *Config) PrimaryRoot() string { + return c.VRoot[0] +} + +type RootConfig []string + +// Decode implements the interface `envdecode.Decoder` +func (r *RootConfig) Decode(repl string) error { + *r = strings.Split(repl, ":") + return nil +} + +type GitHubConfig struct { + Token string `toml:"token,omitempty" env:"GOGH_GITHUB_TOKEN"` + User string `toml:"user,omitempty" env:"GOGH_GITHUB_USER"` + Host string `toml:"host,omitempty" env:"GOGH_GITHUB_HOST"` +} + +var ( + envGoghLogLevel = "GOGH_LOG_LEVEL" + envGoghGitHubUser = "GOGH_GITHUB_USER" + envGoghGitHubToken = "GOGH_GITHUB_TOKEN" + envGoghGitHubHost = "GOGH_GITHUB_HOST" + envGoghRoot = "GOGH_ROOT" + envNames = []string{ + envGoghLogLevel, + envGoghGitHubUser, + envGoghGitHubToken, + envGoghGitHubHost, + envGoghRoot, + } +) + +const ( + // DefaultHost is the default host of the GitHub + DefaultHost = "github.com" + DefaultLogLevel = "warn" +) + +var defaultConfig = Config{ + VLogLevel: DefaultLogLevel, + GitHub: GitHubConfig{ + Host: DefaultHost, + }, +} + +var initDefaultConfig sync.Once + +func DefaultConfig() Config { + initDefaultConfig.Do(func() { + gopaths := filepath.SplitList(build.Default.GOPATH) + root := make([]string, 0, len(gopaths)) + for _, gopath := range gopaths { + root = append(root, filepath.Join(gopath, "src")) + } + defaultConfig.VRoot = unique(root) + }) + return defaultConfig +} + +func LoadFileConfig(filename string) (config Config, err error) { + file, err := os.Open(filename) + switch { + case err == nil: + defer file.Close() + err = toml.NewDecoder(file).Decode(&config) + case os.IsNotExist(err): + err = nil + } + config.VRoot = unique(config.VRoot) + return +} + +func GetEnvarConfig() (config Config, err error) { + err = envdecode.Decode(&config) + config.VRoot = unique(config.VRoot) + return +} + +func MergeConfig(base Config, override ...Config) Config { + c := base + for _, o := range override { + c.VLogLevel = mergeStringOption(c.VLogLevel, o.VLogLevel) + c.VRoot = mergeStringArrayOption(c.VRoot, o.VRoot) + c.GitHub.Token = mergeStringOption(c.GitHub.Token, o.GitHub.Token) + c.GitHub.User = mergeStringOption(c.GitHub.User, o.GitHub.User) + c.GitHub.Host = mergeStringOption(c.GitHub.Host, o.GitHub.Host) + } + return c +} + +func mergeStringOption(base, override string) string { + if override != "" { + return override + } + return base +} + +func mergeStringArrayOption(base, override []string) []string { + if len(override) > 0 { + return override + } + return base +} + +func mergeWriterOption(base, override io.Writer) io.Writer { + if override != nil { + return override + } + return base +} + +func ValidateRoot(root []string) error { + for i, v := range root { + path := filepath.Clean(v) + _, err := os.Stat(path) + switch { + case err == nil: + root[i], err = filepath.EvalSymlinks(path) + if err != nil { + return err + } + case os.IsNotExist(err): + root[i] = path + default: + return err + } + } + + return nil +} +func ConfigGet(ctx gogh.Context, optionName string) error { + return nil +} + +func ConfigGetAll(ctx gogh.Context) error { + return nil +} + +func ConfigSet(config *Config, optionName, optionValue string) (*Config, error) { + return nil, nil +} diff --git a/command/root.go b/command/root.go index b6ee5e4d..a2870acc 100644 --- a/command/root.go +++ b/command/root.go @@ -10,11 +10,11 @@ import ( // Root prints a gogh.root func Root(ctx gogh.Context, all bool) error { if !all { - _, err := fmt.Fprintln(ctx.Stdout(), gogh.PrimaryRoot(ctx)) + _, err := fmt.Fprintln(ctx.Stdout(), ctx.PrimaryRoot()) return err } log.Println("info: Finding all roots...") - for _, root := range ctx.Roots() { + for _, root := range ctx.Root() { if _, err := fmt.Fprintln(ctx.Stdout(), root); err != nil { return err } diff --git a/gogh/util.go b/command/util.go similarity index 93% rename from gogh/util.go rename to command/util.go index 9599cfc6..6b8c0181 100644 --- a/gogh/util.go +++ b/command/util.go @@ -1,4 +1,4 @@ -package gogh +package command func unique(items []string) (uniq []string) { dups := map[string]struct{}{} diff --git a/go.mod b/go.mod index cb943a9e..ef57687f 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,11 @@ require ( github.com/alecthomas/kingpin v0.0.0-20190313214811-7613e5d4efd9 github.com/atotto/clipboard v0.1.1 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/caarlos0/env v0.0.0-20190317132158-240f8632d3d1 github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c github.com/github/hub v2.10.0+incompatible github.com/google/go-github/v24 v24.0.1 + github.com/joeshaw/envdecode v0.0.0-20180312135643-c9e015854467 github.com/karrick/godirwalk v1.8.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/pretty v0.1.0 // indirect @@ -18,7 +20,9 @@ require ( github.com/ogier/pflag v0.0.1 // indirect github.com/pelletier/go-toml v1.2.0 github.com/pkg/errors v0.8.0 + github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 + github.com/wacul/ptr v0.0.0-20190222093950-93c3eb3ee7ea golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect diff --git a/go.sum b/go.sum index 8f05ed82..f4e2ed8c 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,12 @@ github.com/atotto/clipboard v0.1.1 h1:WSoEbAS70E5gw8FbiqFlp69MGsB6dUb4l+0AGGLiVG github.com/atotto/clipboard v0.1.1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/caarlos0/env v0.0.0-20190308142353-9a320ffe30f7 h1:1MK6c94mutWG2rrxhEYpmWk4lZ/U4Tk08DbtPLiGAP8= +github.com/caarlos0/env v0.0.0-20190308142353-9a320ffe30f7/go.mod h1:ARBGf9nGpjcuNQpQLGEsAk8aYO5esFYfNtiKJGx2ioo= +github.com/caarlos0/env v0.0.0-20190317132158-240f8632d3d1 h1:GaPDk7YlXZxu1gXk1jSuGE79IAf4CYIAPN4+/gfQUV0= +github.com/caarlos0/env v0.0.0-20190317132158-240f8632d3d1/go.mod h1:ARBGf9nGpjcuNQpQLGEsAk8aYO5esFYfNtiKJGx2ioo= +github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= +github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c h1:bzYQ6WpR+t35/y19HUkolcg7SYeWZ15IclC9Z4naGHI= github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c/go.mod h1:1WwgAwMKQLYG5I2FBhpVx94YTOAuB2W59IZ7REjSE6Y= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -27,6 +33,8 @@ github.com/google/go-github/v24 v24.0.1 h1:KCt1LjMJEey1qvPXxa9SjaWxwTsCWSq6p2Ju5 github.com/google/go-github/v24 v24.0.1/go.mod h1:CRqaW1Uns1TCkP0wqTpxYyRxRjxwvKU/XSS44u6X74M= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/joeshaw/envdecode v0.0.0-20180312135643-c9e015854467 h1:REcMVvHDwEBFDEUrphtsx+/LqVKkKfcWxnAuVKFzSWk= +github.com/joeshaw/envdecode v0.0.0-20180312135643-c9e015854467/go.mod h1:Q+alOFAXgW5SrcfMPt/G4B2oN+qEcQRJjkn/f4mKL04= github.com/karrick/godirwalk v1.8.0 h1:ycpSqVon/QJJoaT1t8sae0tp1Stg21j+dyuS7OoagcA= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -53,9 +61,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 h1:Ko2LQMrRU+Oy/+EDBwX7eZ2jp3C47eDBB8EIhKTun+I= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/wacul/ptr v0.0.0-20190222093950-93c3eb3ee7ea h1:Dixnoi9TyOD0CJZRVtPkYXgzLmwufCNaO/CD19uJMvQ= +github.com/wacul/ptr v0.0.0-20190222093950-93c3eb3ee7ea/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/gogh/context.go b/gogh/context.go index e5ba2b6c..42ff4a19 100644 --- a/gogh/context.go +++ b/gogh/context.go @@ -2,12 +2,7 @@ package gogh import ( "context" - "fmt" - "go/build" "io" - "log" - "os" - "path/filepath" ) // Context holds configurations and environments @@ -15,49 +10,12 @@ type Context interface { context.Context Stdout() io.Writer Stderr() io.Writer - LogLevel() string - GitHubUser() string GitHubToken() string GitHubHost() string - Roots() []string -} - -func PrimaryRoot(ctx Context) string { - rts := ctx.Roots() - return rts[0] -} - -// Config holds configuration file values. -type Config map[string]interface{} - -// CurrentContext get current context from OS envars and Git configurations -func CurrentContext(ctx context.Context, config Config) (Context, error) { - gitHubUser := getGitHubUser(config) - if gitHubUser == "" { - // Make the error if the GitHubUser is not set. - return nil, fmt.Errorf("failed to find user name. set %s in environment variable", envGoghGitHubUser) - } - if err := ValidateOwner(gitHubUser); err != nil { - return nil, err - } - gitHubToken := getGitHubToken(config) - gitHubHost := getGitHubHost(config) - logLevel := getLogLevel(config) - roots := getRoots(config) - if err := validateRoots(roots); err != nil { - return nil, err - } - return &implContext{ - Context: ctx, - stdout: os.Stdout, - stderr: os.Stderr, - gitHubUser: gitHubUser, - gitHubToken: gitHubToken, - gitHubHost: gitHubHost, - logLevel: logLevel, - roots: roots, - }, nil + LogLevel() string + Root() []string + PrimaryRoot() string } type implContext struct { @@ -68,7 +26,7 @@ type implContext struct { gitHubToken string gitHubHost string logLevel string - roots []string + root []string } func (c *implContext) Stdout() io.Writer { @@ -95,92 +53,10 @@ func (c *implContext) LogLevel() string { return c.logLevel } -func (c *implContext) Roots() []string { - return c.roots +func (c *implContext) Root() []string { + return c.root } -func getConf(def string, envar string, config Config, key string, altEnvars ...string) string { - if val := os.Getenv(envar); val != "" { - log.Printf("debug: Context %s from envar is %q", key, val) - return val - } - if config != nil { - val, ok := config[key] - if ok { - str, ok := val.(string) - if ok { - log.Printf("debug: Context %s from file is %q", key, str) - return str - } else { - log.Printf("warn: Config.%s expects string", confKeyRoot) - } - } - } - for _, e := range altEnvars { - if val := os.Getenv(e); val != "" { - log.Printf("debug: Context %s from alt-envar is %q", key, val) - return val - } - } - return def -} - -func getGitHubToken(config Config) string { - return getConf("", envGoghGitHubToken, config, confKeyGitHubToken, envGitHubToken) -} - -func getGitHubHost(config Config) string { - return getConf(DefaultHost, envGoghGitHubHost, config, confKeyGitHubHost, envGitHubHost) -} - -func getGitHubUser(config Config) string { - return getConf("", envGoghGitHubUser, config, confKeyGitHubUser, envGitHubUser, envUserName) -} - -func getLogLevel(config Config) string { - return getConf(DefaultLogLevel, envGoghLogLevel, config, confKeyLogLevel) -} - -func getRoots(config Config) []string { - envRoot := os.Getenv(envGoghRoot) - if envRoot == "" { - if config != nil { - val, ok := config[confKeyRoot] - if ok { - arr, ok := val.([]string) - if ok { - return unique(arr) - } else { - log.Printf("warn: Config.%s expects string array", confKeyRoot) - } - } - } - gopaths := filepath.SplitList(build.Default.GOPATH) - roots := make([]string, 0, len(gopaths)) - for _, gopath := range gopaths { - roots = append(roots, filepath.Join(gopath, "src")) - } - return unique(roots) - } - return unique(filepath.SplitList(envRoot)) -} - -func validateRoots(roots []string) error { - for i, v := range roots { - path := filepath.Clean(v) - _, err := os.Stat(path) - switch { - case err == nil: - roots[i], err = filepath.EvalSymlinks(path) - if err != nil { - return err - } - case os.IsNotExist(err): - roots[i] = path - default: - return err - } - } - - return nil +func (c *implContext) PrimaryRoot() string { + return c.root[0] } diff --git a/gogh/context_env.go b/gogh/context_env.go deleted file mode 100644 index d77fbd3f..00000000 --- a/gogh/context_env.go +++ /dev/null @@ -1,35 +0,0 @@ -package gogh - -var ( - envGoghLogLevel = "GOGH_LOG_LEVEL" - envGoghGitHubUser = "GOGH_GITHUB_USER" - envGoghGitHubToken = "GOGH_GITHUB_TOKEN" - envGoghGitHubHost = "GOGH_GITHUB_HOST" - envGoghRoot = "GOGH_ROOT" - envGitHubUser = "GITHUB_USER" - envGitHubToken = "GITHUB_TOKEN" - envGitHubHost = "GITHUB_HOST" - envNames = []string{ - envGoghLogLevel, - envGoghGitHubUser, - envGoghGitHubToken, - envGoghGitHubHost, - envGoghRoot, - envGitHubUser, - envGitHubToken, - envGitHubHost, - envUserName, - } -) - -const ( - // DefaultHost is the default host of the GitHub - DefaultHost = "github.com" - DefaultLogLevel = "warn" - - confKeyLogLevel = "LogLevel" - confKeyGitHubUser = "GitHubUser" - confKeyGitHubToken = "GitHubToken" - confKeyGitHubHost = "GitHubHost" - confKeyRoot = "Root" -) diff --git a/gogh/context_env_others.go b/gogh/context_env_others.go deleted file mode 100644 index feeaa2ed..00000000 --- a/gogh/context_env_others.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build !windows - -package gogh - -var ( - envUserName = "USER" -) diff --git a/gogh/context_env_windows.go b/gogh/context_env_windows.go deleted file mode 100644 index 22e035f5..00000000 --- a/gogh/context_env_windows.go +++ /dev/null @@ -1,3 +0,0 @@ -package gogh - -var envUserName = "USERNAME" diff --git a/gogh/context_test.go b/gogh/context_test.go deleted file mode 100644 index 2097b009..00000000 --- a/gogh/context_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package gogh - -import ( - "context" - "go/build" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func resetEnv(t *testing.T) { - t.Helper() - for _, key := range envNames { - require.NoError(t, os.Setenv(key, "")) - } -} - -func TestContext(t *testing.T) { - t.Run("get context from envar (without configuration)", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) - require.NoError(t, os.Setenv(envGitHubToken, "tokenx2")) // alt: never used - require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) - require.NoError(t, os.Setenv(envGitHubHost, "hostx2")) // alt: never used - require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) - require.NoError(t, os.Setenv(envGitHubUser, "kyoh87")) // alt: never used - require.NoError(t, os.Setenv(envUserName, "kyoh88")) // alt: never used - require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) - require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) - require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) - ctx, err := CurrentContext(context.Background(), nil) - require.NoError(t, err) - assert.Equal(t, "tokenx1", ctx.GitHubToken()) - assert.Equal(t, "hostx1", ctx.GitHubHost()) - assert.Equal(t, "kyoh86", ctx.GitHubUser()) - assert.Equal(t, "trace", ctx.LogLevel()) - assert.Equal(t, []string{"/foo", "/bar"}, ctx.Roots()) - }) - - t.Run("get context from envar (with configuration)", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) - require.NoError(t, os.Setenv(envGitHubToken, "tokenx2")) // alt: never used - require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) - require.NoError(t, os.Setenv(envGitHubHost, "hostx2")) // alt: never used - require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) - require.NoError(t, os.Setenv(envGitHubUser, "kyoh87")) // alt: never used - require.NoError(t, os.Setenv(envUserName, "kyoh88")) // alt: never used - require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) - require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) - ctx, err := CurrentContext(context.Background(), Config{ // config: never used - confKeyGitHubToken: "tokenx2", - confKeyGitHubHost: "hostx2", - confKeyGitHubUser: "kyoh87", - confKeyLogLevel: "error", - confKeyRoot: []string{"/baz", "/bux"}, - }) - require.NoError(t, err) - assert.Equal(t, "tokenx1", ctx.GitHubToken()) - assert.Equal(t, "hostx1", ctx.GitHubHost()) - assert.Equal(t, "kyoh86", ctx.GitHubUser()) - assert.Equal(t, "trace", ctx.LogLevel()) - assert.Equal(t, []string{"/foo", "/bar"}, ctx.Roots()) - }) - - t.Run("get context from configuration", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGitHubToken, "tokenx2")) // alt: never used - require.NoError(t, os.Setenv(envGitHubHost, "hostx2")) // alt: never used - require.NoError(t, os.Setenv(envGitHubUser, "kyoh87")) // alt: never used - require.NoError(t, os.Setenv(envUserName, "kyoh88")) // alt: never used - ctx, err := CurrentContext(context.Background(), Config{ - confKeyGitHubToken: "tokenx1", - confKeyGitHubHost: "hostx1", - confKeyGitHubUser: "kyoh86", - confKeyLogLevel: "error", - confKeyRoot: []string{"/baz", "/bux"}, - }) - require.NoError(t, err) - assert.Equal(t, "tokenx1", ctx.GitHubToken()) - assert.Equal(t, "hostx1", ctx.GitHubHost()) - assert.Equal(t, "kyoh86", ctx.GitHubUser()) - assert.Equal(t, "error", ctx.LogLevel()) - assert.Equal(t, []string{"/baz", "/bux"}, ctx.Roots()) - }) - - t.Run("get context from alt envars", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGitHubToken, "tokenx1")) - require.NoError(t, os.Setenv(envGitHubHost, "hostx1")) - require.NoError(t, os.Setenv(envGitHubUser, "kyoh86")) - require.NoError(t, os.Setenv(envUserName, "kyoh87")) // low priority: never used - ctx, err := CurrentContext(context.Background(), nil) - require.NoError(t, err) - assert.Equal(t, "tokenx1", ctx.GitHubToken()) - assert.Equal(t, "hostx1", ctx.GitHubHost()) - assert.Equal(t, "kyoh86", ctx.GitHubUser()) - }) - - t.Run("get default context", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) - ctx, err := CurrentContext(context.Background(), nil) - require.NoError(t, err) - assert.Equal(t, "", ctx.GitHubToken()) - assert.Equal(t, DefaultHost, ctx.GitHubHost()) - assert.Equal(t, "warn", ctx.LogLevel()) - assert.Equal(t, []string{filepath.Join(build.Default.GOPATH, "src")}, ctx.Roots()) - }) - - t.Run("expect to fail to get user name from anywhere", func(t *testing.T) { - resetEnv(t) - _, err := CurrentContext(context.Background(), nil) - require.EqualError(t, err, "failed to find user name. set GOGH_GITHUB_USER in environment variable") - }) - - t.Run("expect to get invalid user name", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envUserName, "-kyoh88")) - _, err := CurrentContext(context.Background(), nil) - require.EqualError(t, err, "owner name may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen") - }) - - t.Run("expects roots are not duplicated", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar:/bar:/foo")) - assert.Equal(t, []string{"/foo", "/bar"}, getRoots(nil)) - }) -} diff --git a/gogh/local.go b/gogh/local.go index 63bb8378..4ef25b5f 100644 --- a/gogh/local.go +++ b/gogh/local.go @@ -70,7 +70,7 @@ func NewProject(ctx Context, repo *Repo) (*Project, error) { return nil, err } relPath := repo.RelPath(ctx) - fullPath := filepath.Join(PrimaryRoot(ctx), relPath) + fullPath := filepath.Join(ctx.PrimaryRoot(), relPath) return &Project{ FullPath: fullPath, RelPath: relPath, @@ -102,7 +102,7 @@ func (p *Project) Subpaths() []string { // IsInPrimaryRoot check which the repository is in primary root directory for gogh func (p *Project) IsInPrimaryRoot(ctx Context) bool { - return strings.HasPrefix(p.FullPath, PrimaryRoot(ctx)) + return strings.HasPrefix(p.FullPath, ctx.PrimaryRoot()) } func isVcsDir(path string) bool { @@ -180,12 +180,12 @@ func parseProject(ctx Context, root string, fullPath string) (*Project, error) { // WalkInPrimary thorugh projects (local repositories) in the first gogh.root directory func WalkInPrimary(ctx Context, callback WalkFunc) error { - return walkInPath(ctx, PrimaryRoot(ctx), callback) + return walkInPath(ctx, ctx.PrimaryRoot(), callback) } // Walk thorugh projects (local repositories) in gogh.root directories func Walk(ctx Context, callback WalkFunc) error { - for _, root := range ctx.Roots() { + for _, root := range ctx.Root() { if err := walkInPath(ctx, root, callback); err != nil { return err } diff --git a/gogh/local_test.go b/gogh/local_test.go index 34f4d997..25ee92bd 100644 --- a/gogh/local_test.go +++ b/gogh/local_test.go @@ -14,7 +14,7 @@ import ( func TestParseProject(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - ctx := implContext{roots: []string{tmp}, gitHubHost: "github.com"} + ctx := implContext{root: []string{tmp}, gitHubHost: "github.com"} t.Run("in primary root", func(t *testing.T) { path := filepath.Join(tmp, "github.com", "kyoh86", "gogh") @@ -29,7 +29,7 @@ func TestParseProject(t *testing.T) { tmp2, err := ioutil.TempDir(os.TempDir(), "gogh-test2") require.NoError(t, err) ctx := ctx - ctx.roots = append(ctx.roots, tmp2) + ctx.root = append(ctx.root, tmp2) path := filepath.Join(tmp2, "github.com", "kyoh86", "gogh") p, err := parseProject(&ctx, tmp2, path) require.NoError(t, err) @@ -67,7 +67,7 @@ func TestParseProject(t *testing.T) { func TestFindOrNewProject(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - ctx := implContext{roots: []string{tmp}, gitHubUser: "kyoh86", gitHubHost: "github.com"} + ctx := implContext{root: []string{tmp}, gitHubUser: "kyoh86", gitHubHost: "github.com"} path := filepath.Join(tmp, "github.com", "kyoh86", "gogh") @@ -142,13 +142,13 @@ func TestWalk(t *testing.T) { } t.Run("Not existing root", func(t *testing.T) { t.Run("primary root", func(t *testing.T) { - ctx := implContext{roots: []string{"/that/will/never/exist"}} + ctx := implContext{root: []string{"/that/will/never/exist"}} require.NoError(t, Walk(&ctx, neverCalled(t))) }) t.Run("secondary root", func(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - ctx := implContext{roots: []string{tmp, "/that/will/never/exist"}} + ctx := implContext{root: []string{tmp, "/that/will/never/exist"}} require.NoError(t, Walk(&ctx, neverCalled(t))) }) }) @@ -158,7 +158,7 @@ func TestWalk(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) require.NoError(t, ioutil.WriteFile(filepath.Join(tmp, "foo"), nil, 0644)) - ctx := implContext{roots: []string{filepath.Join(tmp, "foo")}} + ctx := implContext{root: []string{filepath.Join(tmp, "foo")}} require.NoError(t, Walk(&ctx, neverCalled(t))) require.NoError(t, WalkInPrimary(&ctx, neverCalled(t))) }) @@ -166,7 +166,7 @@ func TestWalk(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) require.NoError(t, ioutil.WriteFile(filepath.Join(tmp, "foo"), nil, 0644)) - ctx := implContext{roots: []string{tmp, filepath.Join(tmp, "foo")}} + ctx := implContext{root: []string{tmp, filepath.Join(tmp, "foo")}} require.NoError(t, Walk(&ctx, neverCalled(t))) require.NoError(t, WalkInPrimary(&ctx, neverCalled(t))) }) @@ -178,7 +178,7 @@ func TestWalk(t *testing.T) { path := filepath.Join(tmp, "github.com", "kyoh--86", "gogh") require.NoError(t, os.MkdirAll(filepath.Join(path, ".git"), 0755)) - ctx := implContext{roots: []string{tmp, filepath.Join(tmp)}} + ctx := implContext{root: []string{tmp, filepath.Join(tmp)}} assert.NoError(t, Walk(&ctx, neverCalled(t))) }) @@ -189,7 +189,7 @@ func TestWalk(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(path, ".git"), 0755)) require.NoError(t, ioutil.WriteFile(filepath.Join(tmp, "foo"), nil, 0644)) - ctx := implContext{roots: []string{tmp, filepath.Join(tmp, "foo")}, gitHubHost: "github.com"} + ctx := implContext{root: []string{tmp, filepath.Join(tmp, "foo")}, gitHubHost: "github.com"} err = errors.New("sample error") assert.EqualError(t, Walk(&ctx, func(p *Project) error { assert.Equal(t, path, p.FullPath) @@ -206,7 +206,7 @@ func TestList_Symlink(t *testing.T) { symDir, err := ioutil.TempDir("", "") require.NoError(t, err) - ctx := &implContext{roots: []string{root}, gitHubHost: "github.com"} + ctx := &implContext{root: []string{root}, gitHubHost: "github.com"} err = os.MkdirAll(filepath.Join(root, "github.com", "atom", "atom", ".git"), 0777) require.NoError(t, err) @@ -240,7 +240,7 @@ func TestQuery(t *testing.T) { path5 := filepath.Join(root2, "github.com", "kyoh86", "gogh") require.NoError(t, os.MkdirAll(filepath.Join(path5, ".git"), 0755)) - ctx := implContext{roots: []string{root1, root2}, gitHubHost: "github.com"} + ctx := implContext{root: []string{root1, root2}, gitHubHost: "github.com"} assert.NoError(t, Query(&ctx, "never found", Walk, func(*Project) error { t.Fatal("should not be called but...") diff --git a/gogh/remote/client.go b/gogh/remote/client.go index 4f2eded9..f74c240c 100644 --- a/gogh/remote/client.go +++ b/gogh/remote/client.go @@ -11,7 +11,7 @@ import ( // NewClient builds GitHub Client with GitHub API token that is configured. func NewClient(ctx gogh.Context) (*github.Client, error) { - if host := ctx.GitHubHost(); host != "" && host != gogh.DefaultHost { + if host := ctx.GitHubHost(); host != "" && host != "github.com" { url := fmt.Sprintf("https://%s/api/v3", host) return github.NewEnterpriseClient(url, url, oauth2Client(ctx)) } diff --git a/gogh/repo.go b/gogh/repo.go index a57cac8a..9354376a 100644 --- a/gogh/repo.go +++ b/gogh/repo.go @@ -103,7 +103,7 @@ func (r *Repo) Set(rawRepo string) error { path = strings.Trim(url.Path, "/") } else { r.scheme = "https" - r.host = DefaultHost + r.host = "" // use default value r.user = nil path = url.Path } @@ -141,7 +141,10 @@ func (r *Repo) Scheme(_ Context) string { } // Host returns host of the repository -func (r *Repo) Host(_ Context) string { +func (r *Repo) Host(ctx Context) string { + if r.host == "" { + return ctx.GitHubHost() + } return r.host } diff --git a/gogh/repo_test.go b/gogh/repo_test.go index 4f53a7c5..dfa65028 100644 --- a/gogh/repo_test.go +++ b/gogh/repo_test.go @@ -46,7 +46,7 @@ func TestRepo(t *testing.T) { }) t.Run("owner/name spec", func(t *testing.T) { - ctx := &implContext{} + ctx := &implContext{gitHubHost: "github.com"} spec, err := ParseRepo("kyoh86/gogh") require.NoError(t, err) assert.Equal(t, "kyoh86/gogh", spec.FullName(ctx)) @@ -55,7 +55,7 @@ func TestRepo(t *testing.T) { }) t.Run("name only spec", func(t *testing.T) { - ctx := &implContext{gitHubUser: "kyoh86"} + ctx := &implContext{gitHubUser: "kyoh86", gitHubHost: "github.com"} spec, err := ParseRepo("gogh") require.NoError(t, err) assert.Equal(t, "kyoh86/gogh", spec.FullName(ctx)) diff --git a/internal/mainutil/mainutil.go b/internal/mainutil/mainutil.go new file mode 100644 index 00000000..cd7af0ba --- /dev/null +++ b/internal/mainutil/mainutil.go @@ -0,0 +1,50 @@ +package mainutil + +import ( + "path/filepath" + + "github.com/alecthomas/kingpin" + "github.com/comail/colog" + "github.com/kyoh86/gogh/command" + "github.com/kyoh86/gogh/gogh" + "github.com/kyoh86/xdg" +) + +func SetConfigFlag(cmd *kingpin.CmdClause, configFile *string) { + cmd.Flag("config", "configuration file"). + Default(filepath.Join(xdg.CacheHome(), "gogh", "config.toml")). + Envar("GOGH_CONFIG"). + StringVar(configFile) +} + +func InitLog(ctx gogh.Context) error { + lvl, err := colog.ParseLevel(ctx.LogLevel()) + if err != nil { + return err + } + colog.Register() + colog.SetOutput(ctx.Stderr()) + colog.SetMinLevel(lvl) + colog.SetDefaultLevel(colog.LError) + return nil +} + +func WrapCommand(cmd *kingpin.CmdClause, f func(gogh.Context) error) (string, func() error) { + var configFile string + SetConfigFlag(cmd, &configFile) + return cmd.FullCommand(), func() error { + fileConfig, err := command.LoadFileConfig(configFile) + if err != nil { + return err + } + envarConfig, err := command.GetEnvarConfig() + if err != nil { + return err + } + + ctx := command.MergeConfig(command.DefaultConfig(), fileConfig, envarConfig) + + InitLog(&ctx) + return f(&ctx) + } +} diff --git a/main.go b/main.go index 4c47ff72..0ff175c9 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,15 @@ package main import ( - "context" "fmt" "log" "net/url" "os" - "path/filepath" "github.com/alecthomas/kingpin" - "github.com/comail/colog" "github.com/kyoh86/gogh/command" "github.com/kyoh86/gogh/gogh" - "github.com/kyoh86/xdg" - toml "github.com/pelletier/go-toml" + "github.com/kyoh86/gogh/internal/mainutil" ) var ( @@ -23,28 +19,15 @@ var ( ) func main() { - var configFile string - var config gogh.Config - app := kingpin.New("gogh", "GO GitHub project manager").Version(fmt.Sprintf("%s-%s (%s)", version, commit, date)).Author("kyoh86") - app.Flag("config", "configuration file"). - Default(filepath.Join(xdg.CacheHome(), "gogh", "config.yml")). - Envar("GOGH_CONFIG"). - Action(func(ctx *kingpin.ParseContext) error { - file, err := os.Open(configFile) - switch { - case err == nil: - // noop - case os.IsNotExist(err): - return nil - default: - return err - } - return toml.NewDecoder(file).Decode(&config) - }).StringVar(&configFile) + app.Command("config", "Get and set options") cmds := map[string]func() error{} - for _, f := range []func(*kingpin.Application, gogh.Config) (string, func() error){ + for _, f := range []func(*kingpin.Application) (string, func() error){ + configGet, + configGetAll, + configSet, + get, bulk, pipe, @@ -59,7 +42,7 @@ func main() { repos, } { - key, run := f(app, config) + key, run := f(app) cmds[key] = run } if err := cmds[kingpin.MustParse(app.Parse(os.Args[1:]))](); err != nil { @@ -67,25 +50,60 @@ func main() { } } -func wrapContext(config gogh.Config, f func(gogh.Context) error) func() error { - return func() error { - ctx, err := gogh.CurrentContext(context.Background(), config) +func configGet(app *kingpin.Application) (string, func() error) { + var ( + name string + ) + cmd := app.GetCommand("config").Command("get", "get an option") + cmd.Arg("name", "option name").Required().StringVar(&name) + + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { + return command.ConfigGet(ctx, name) + }) +} + +func configGetAll(app *kingpin.Application) (string, func() error) { + cmd := app.GetCommand("config").Command("get-all", "get all options") + + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { + return command.ConfigGetAll(ctx) + }) +} + +func configSet(app *kingpin.Application) (string, func() error) { + var ( + name string + value string + configFile string + ) + cmd := app.GetCommand("config").Command("set", "set an option") + cmd.Arg("name", "option name").Required().StringVar(&name) + cmd.Arg("value", "option value").Required().StringVar(&value) + + mainutil.SetConfigFlag(cmd, &configFile) + + return cmd.FullCommand(), func() error { + fileConfig, err := command.LoadFileConfig(configFile) + if err != nil { + return err + } + envarConfig, err := command.GetEnvarConfig() if err != nil { return err } - lvl, err := colog.ParseLevel(ctx.LogLevel()) + ctx := command.MergeConfig(command.DefaultConfig(), fileConfig, envarConfig) + mainutil.InitLog(&ctx) + config, err := command.ConfigSet(&fileConfig, name, value) if err != nil { return err } - colog.Register() - colog.SetOutput(ctx.Stderr()) - colog.SetMinLevel(lvl) - colog.SetDefaultLevel(colog.LError) - return f(ctx) + _ = config + //TODO: save config + return nil } } -func get(app *kingpin.Application, config gogh.Config) (string, func() error) { +func get(app *kingpin.Application) (string, func() error) { var ( update bool withSSH bool @@ -98,12 +116,12 @@ func get(app *kingpin.Application, config gogh.Config) (string, func() error) { cmd.Flag("shallow", "Do a shallow clone").BoolVar(&shallow) cmd.Arg("repositories", "Target repositories ( | / | )").Required().SetValue(&repoNames) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.GetAll(ctx, update, withSSH, shallow, repoNames) }) } -func bulk(app *kingpin.Application, config gogh.Config) (string, func() error) { +func bulk(app *kingpin.Application) (string, func() error) { var ( update bool withSSH bool @@ -114,12 +132,12 @@ func bulk(app *kingpin.Application, config gogh.Config) (string, func() error) { cmd.Flag("ssh", "Clone with SSH").BoolVar(&withSSH) cmd.Flag("shallow", "Do a shallow clone").BoolVar(&shallow) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.Bulk(ctx, update, withSSH, shallow) }) } -func pipe(app *kingpin.Application, config gogh.Config) (string, func() error) { +func pipe(app *kingpin.Application) (string, func() error) { var ( update bool withSSH bool @@ -134,12 +152,12 @@ func pipe(app *kingpin.Application, config gogh.Config) (string, func() error) { cmd.Arg("command", "Subcommand calling to get import paths").StringVar(&srcCmd) cmd.Arg("command-args", "Arguments that will be passed to subcommand").StringsVar(&srcCmdArgs) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.Pipe(ctx, update, withSSH, shallow, srcCmd, srcCmdArgs) }) } -func fork(app *kingpin.Application, config gogh.Config) (string, func() error) { +func fork(app *kingpin.Application) (string, func() error) { var ( update bool withSSH bool @@ -158,12 +176,12 @@ func fork(app *kingpin.Application, config gogh.Config) (string, func() error) { cmd.Flag("org", "Fork the repository within this organization").PlaceHolder("ORGANIZATION").StringVar(&organization) cmd.Arg("repository", "Target repository ( | / | )").Required().SetValue(&repo) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.Fork(ctx, update, withSSH, shallow, noRemote, remoteName, organization, &repo) }) } -func create(app *kingpin.Application, config gogh.Config) (string, func() error) { +func create(app *kingpin.Application) (string, func() error) { var ( private bool description string @@ -188,12 +206,12 @@ func create(app *kingpin.Application, config gogh.Config) (string, func() error) cmd.Flag("shared", "Specify that the Git repository is to be shared amongst several users.").SetValue(&shared) cmd.Arg("repository", "Target repository ( | / | )").Required().SetValue(&repo) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.New(ctx, private, description, homepage, browse, clip, bare, template, separateGitDir, shared, &repo) }) } -func where(app *kingpin.Application, config gogh.Config) (string, func() error) { +func where(app *kingpin.Application) (string, func() error) { var ( primary bool exact bool @@ -204,12 +222,12 @@ func where(app *kingpin.Application, config gogh.Config) (string, func() error) cmd.Flag("exact", "Specifies name of the project in query").Short('e').BoolVar(&exact) cmd.Arg("query", "Project name query").StringVar(&query) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.Where(ctx, primary, exact, query) }) } -func list(app *kingpin.Application, config gogh.Config) (string, func() error) { +func list(app *kingpin.Application) (string, func() error) { var ( format string primary bool @@ -220,12 +238,12 @@ func list(app *kingpin.Application, config gogh.Config) (string, func() error) { cmd.Flag("primary", "Only in primary root directory").Short('p').BoolVar(&primary) cmd.Arg("query", "Project name query").StringVar(&query) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.List(ctx, gogh.ProjectListFormat(format), primary, query) }) } -func dump(app *kingpin.Application, config gogh.Config) (string, func() error) { +func dump(app *kingpin.Application) (string, func() error) { var ( primary bool query string @@ -234,12 +252,12 @@ func dump(app *kingpin.Application, config gogh.Config) (string, func() error) { cmd.Flag("primary", "Only in primary root directory").Short('p').BoolVar(&primary) cmd.Arg("query", "Project name query").StringVar(&query) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.List(ctx, gogh.ProjectListFormatURL, primary, query) }) } -func find(app *kingpin.Application, config gogh.Config) (string, func() error) { +func find(app *kingpin.Application) (string, func() error) { var ( primary bool query string @@ -248,22 +266,22 @@ func find(app *kingpin.Application, config gogh.Config) (string, func() error) { cmd.Flag("primary", "Only in primary root directory").Short('p').BoolVar(&primary) cmd.Arg("query", "Project name query").StringVar(&query) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.Where(ctx, primary, true, query) }) } -func root(app *kingpin.Application, config gogh.Config) (string, func() error) { +func root(app *kingpin.Application) (string, func() error) { var all bool cmd := app.Command("root", "Show repositories' root") cmd.Flag("all", "Show all roots").Envar("GOGH_FLAG_ROOT_ALL").BoolVar(&all) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.Root(ctx, all) }) } -func setup(app *kingpin.Application, config gogh.Config) (string, func() error) { +func setup(app *kingpin.Application) (string, func() error) { var ( cdFuncName string shell string @@ -272,12 +290,12 @@ func setup(app *kingpin.Application, config gogh.Config) (string, func() error) cmd.Flag("cd-function-name", "Name of the function to define").Default("gogogh").Hidden().StringVar(&cdFuncName) cmd.Flag("shell", "Target shell path").Envar("SHELL").Hidden().StringVar(&shell) - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.Setup(ctx, cdFuncName, shell) }) } -func repos(app *kingpin.Application, config gogh.Config) (string, func() error) { +func repos(app *kingpin.Application) (string, func() error) { var ( user string own bool @@ -296,7 +314,7 @@ func repos(app *kingpin.Application, config gogh.Config) (string, func() error) cmd.Flag("sort", "Sort repositories by").Default("full_name").EnumVar(&sort, "created", "updated", "pushed", "full_name") cmd.Flag("direction", "Sort direction").Default("default").EnumVar(&direction, "asc", "desc", "default") - return cmd.FullCommand(), wrapContext(config, func(ctx gogh.Context) error { + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { return command.Repos(ctx, user, own, collaborate, member, visibility, sort, direction) }) } From 983814ec4f98a1a4ec8ba29f11b8c886573ea07b Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Tue, 26 Mar 2019 09:16:04 +0900 Subject: [PATCH 05/30] refactor: move command/config => gogh/config --- command/config.go | 179 +--------------------------------- gogh/config.go | 160 ++++++++++++++++++++++++++++++ gogh/config_test.go | 59 +++++++++++ gogh/local.go | 2 +- gogh/repo.go | 30 +----- {command => gogh}/util.go | 2 +- gogh/validation.go | 62 ++++++++++++ internal/mainutil/mainutil.go | 43 ++++++-- main.go | 23 +---- 9 files changed, 326 insertions(+), 234 deletions(-) create mode 100644 gogh/config.go create mode 100644 gogh/config_test.go rename {command => gogh}/util.go (93%) create mode 100644 gogh/validation.go diff --git a/command/config.go b/command/config.go index 47e37adf..b4cf4a73 100644 --- a/command/config.go +++ b/command/config.go @@ -1,186 +1,9 @@ package command import ( - "context" - "go/build" - "io" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/joeshaw/envdecode" "github.com/kyoh86/gogh/gogh" - "github.com/pelletier/go-toml" ) -// Config holds configuration file values. -type Config struct { - context.Context - VLogLevel string `toml:"loglevel,omitempty" env:"GOGH_LOG_LEVEL"` - VRoot RootConfig `toml:"root,omitempty" env:"GOGH_ROOT" envSeparator:":"` - GitHub GitHubConfig `toml:"github,omitempty"` -} - -func (c *Config) Stdout() io.Writer { - return os.Stdout -} - -func (c *Config) Stderr() io.Writer { - return os.Stderr -} - -func (c *Config) GitHubUser() string { - return c.GitHub.User -} - -func (c *Config) GitHubToken() string { - return c.GitHub.Token -} - -func (c *Config) GitHubHost() string { - return c.GitHub.Host -} - -func (c *Config) LogLevel() string { - return c.VLogLevel -} - -func (c *Config) Root() []string { - return c.VRoot -} - -func (c *Config) PrimaryRoot() string { - return c.VRoot[0] -} - -type RootConfig []string - -// Decode implements the interface `envdecode.Decoder` -func (r *RootConfig) Decode(repl string) error { - *r = strings.Split(repl, ":") - return nil -} - -type GitHubConfig struct { - Token string `toml:"token,omitempty" env:"GOGH_GITHUB_TOKEN"` - User string `toml:"user,omitempty" env:"GOGH_GITHUB_USER"` - Host string `toml:"host,omitempty" env:"GOGH_GITHUB_HOST"` -} - -var ( - envGoghLogLevel = "GOGH_LOG_LEVEL" - envGoghGitHubUser = "GOGH_GITHUB_USER" - envGoghGitHubToken = "GOGH_GITHUB_TOKEN" - envGoghGitHubHost = "GOGH_GITHUB_HOST" - envGoghRoot = "GOGH_ROOT" - envNames = []string{ - envGoghLogLevel, - envGoghGitHubUser, - envGoghGitHubToken, - envGoghGitHubHost, - envGoghRoot, - } -) - -const ( - // DefaultHost is the default host of the GitHub - DefaultHost = "github.com" - DefaultLogLevel = "warn" -) - -var defaultConfig = Config{ - VLogLevel: DefaultLogLevel, - GitHub: GitHubConfig{ - Host: DefaultHost, - }, -} - -var initDefaultConfig sync.Once - -func DefaultConfig() Config { - initDefaultConfig.Do(func() { - gopaths := filepath.SplitList(build.Default.GOPATH) - root := make([]string, 0, len(gopaths)) - for _, gopath := range gopaths { - root = append(root, filepath.Join(gopath, "src")) - } - defaultConfig.VRoot = unique(root) - }) - return defaultConfig -} - -func LoadFileConfig(filename string) (config Config, err error) { - file, err := os.Open(filename) - switch { - case err == nil: - defer file.Close() - err = toml.NewDecoder(file).Decode(&config) - case os.IsNotExist(err): - err = nil - } - config.VRoot = unique(config.VRoot) - return -} - -func GetEnvarConfig() (config Config, err error) { - err = envdecode.Decode(&config) - config.VRoot = unique(config.VRoot) - return -} - -func MergeConfig(base Config, override ...Config) Config { - c := base - for _, o := range override { - c.VLogLevel = mergeStringOption(c.VLogLevel, o.VLogLevel) - c.VRoot = mergeStringArrayOption(c.VRoot, o.VRoot) - c.GitHub.Token = mergeStringOption(c.GitHub.Token, o.GitHub.Token) - c.GitHub.User = mergeStringOption(c.GitHub.User, o.GitHub.User) - c.GitHub.Host = mergeStringOption(c.GitHub.Host, o.GitHub.Host) - } - return c -} - -func mergeStringOption(base, override string) string { - if override != "" { - return override - } - return base -} - -func mergeStringArrayOption(base, override []string) []string { - if len(override) > 0 { - return override - } - return base -} - -func mergeWriterOption(base, override io.Writer) io.Writer { - if override != nil { - return override - } - return base -} - -func ValidateRoot(root []string) error { - for i, v := range root { - path := filepath.Clean(v) - _, err := os.Stat(path) - switch { - case err == nil: - root[i], err = filepath.EvalSymlinks(path) - if err != nil { - return err - } - case os.IsNotExist(err): - root[i] = path - default: - return err - } - } - - return nil -} func ConfigGet(ctx gogh.Context, optionName string) error { return nil } @@ -189,6 +12,6 @@ func ConfigGetAll(ctx gogh.Context) error { return nil } -func ConfigSet(config *Config, optionName, optionValue string) (*Config, error) { +func ConfigSet(config *gogh.Config, optionName, optionValue string) (*gogh.Config, error) { return nil, nil } diff --git a/gogh/config.go b/gogh/config.go new file mode 100644 index 00000000..d85cd62b --- /dev/null +++ b/gogh/config.go @@ -0,0 +1,160 @@ +package gogh + +import ( + "context" + "go/build" + "io" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/joeshaw/envdecode" + "github.com/pelletier/go-toml" +) + +// Config holds configuration file values. +type Config struct { + context.Context + VLogLevel string `toml:"loglevel,omitempty" env:"GOGH_LOG_LEVEL"` + VRoot RootConfig `toml:"root,omitempty" env:"GOGH_ROOT"` + GitHub GitHubConfig `toml:"github,omitempty"` +} + +func (c *Config) Stdout() io.Writer { + return os.Stdout +} + +func (c *Config) Stderr() io.Writer { + return os.Stderr +} + +func (c *Config) GitHubUser() string { + return c.GitHub.User +} + +func (c *Config) GitHubToken() string { + return c.GitHub.Token +} + +func (c *Config) GitHubHost() string { + return c.GitHub.Host +} + +func (c *Config) LogLevel() string { + return c.VLogLevel +} + +func (c *Config) Root() []string { + return c.VRoot +} + +func (c *Config) PrimaryRoot() string { + return c.VRoot[0] +} + +type RootConfig []string + +// Decode implements the interface `envdecode.Decoder` +func (r *RootConfig) Decode(repl string) error { + *r = strings.Split(repl, ":") + return nil +} + +type GitHubConfig struct { + Token string `toml:"token,omitempty" env:"GOGH_GITHUB_TOKEN"` + User string `toml:"user,omitempty" env:"GOGH_GITHUB_USER"` + Host string `toml:"host,omitempty" env:"GOGH_GITHUB_HOST"` +} + +var ( + envGoghLogLevel = "GOGH_LOG_LEVEL" + envGoghGitHubUser = "GOGH_GITHUB_USER" + envGoghGitHubToken = "GOGH_GITHUB_TOKEN" + envGoghGitHubHost = "GOGH_GITHUB_HOST" + envGoghRoot = "GOGH_ROOT" + envNames = []string{ + envGoghLogLevel, + envGoghGitHubUser, + envGoghGitHubToken, + envGoghGitHubHost, + envGoghRoot, + } +) + +const ( + // DefaultHost is the default host of the GitHub + DefaultHost = "github.com" + DefaultLogLevel = "warn" +) + +var defaultConfig = Config{ + VLogLevel: DefaultLogLevel, + GitHub: GitHubConfig{ + Host: DefaultHost, + }, +} + +var initDefaultConfig sync.Once + +func DefaultConfig() *Config { + initDefaultConfig.Do(func() { + gopaths := filepath.SplitList(build.Default.GOPATH) + root := make([]string, 0, len(gopaths)) + for _, gopath := range gopaths { + root = append(root, filepath.Join(gopath, "src")) + } + defaultConfig.VRoot = unique(root) + }) + return &defaultConfig +} + +func LoadFileConfig(filename string) (config *Config, err error) { + file, err := os.Open(filename) + switch { + case err == nil: + defer file.Close() + config = &Config{} + err = toml.NewDecoder(file).Decode(config) + case os.IsNotExist(err): + err = nil + } + config.VRoot = unique(config.VRoot) + return +} + +func GetEnvarConfig() (config *Config, err error) { + config = &Config{} + err = envdecode.Decode(config) + if err == envdecode.ErrNoTargetFieldsAreSet { + err = nil + } + config.VRoot = unique(config.VRoot) + return +} + +func MergeConfig(base *Config, override ...*Config) *Config { + c := base + for _, o := range override { + c.VLogLevel = mergeStringOption(c.VLogLevel, o.VLogLevel) + c.VRoot = mergeStringArrayOption(c.VRoot, o.VRoot) + c.GitHub.Token = mergeStringOption(c.GitHub.Token, o.GitHub.Token) + c.GitHub.User = mergeStringOption(c.GitHub.User, o.GitHub.User) + c.GitHub.Host = mergeStringOption(c.GitHub.Host, o.GitHub.Host) + } + return c +} + +func mergeStringOption(base, override string) string { + if override != "" { + return override + } + return base +} + +func mergeStringArrayOption(base, override []string) []string { + if len(override) > 0 { + return override + } + return base +} diff --git a/gogh/config_test.go b/gogh/config_test.go new file mode 100644 index 00000000..a18fedf9 --- /dev/null +++ b/gogh/config_test.go @@ -0,0 +1,59 @@ +package gogh + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfig(t *testing.T) { + resetEnv := func(t *testing.T) { + t.Helper() + for _, key := range envNames { + require.NoError(t, os.Setenv(key, "")) + } + } + + t.Run("get context without roots", func(t *testing.T) { + resetEnv(t) + require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) + require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) + require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) + require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) + + cfg, err := GetEnvarConfig() + require.NoError(t, err) + assert.Equal(t, "tokenx1", cfg.GitHubToken()) + assert.Equal(t, "hostx1", cfg.GitHubHost()) + assert.Equal(t, "kyoh86", cfg.GitHubUser()) + assert.Equal(t, "trace", cfg.LogLevel()) + }) + + t.Run("expect to get invalid user name", func(t *testing.T) { + resetEnv(t) + require.NoError(t, os.Setenv(envGoghGitHubUser, "-kyoh88")) + cfg, err := GetEnvarConfig() + require.NoError(t, err) + require.NotNil(t, ValidateContext(cfg)) + }) + + t.Run("get root paths", func(t *testing.T) { + resetEnv(t) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) + cfg, err := GetEnvarConfig() + require.NoError(t, err) + assert.NoError(t, err) + assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) + }) + + t.Run("expects roots are not duplicated", func(t *testing.T) { + resetEnv(t) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar:/bar:/foo")) + cfg, err := GetEnvarConfig() + require.NoError(t, err) + assert.NoError(t, err) + assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) + }) +} diff --git a/gogh/local.go b/gogh/local.go index 4ef25b5f..9ccb7573 100644 --- a/gogh/local.go +++ b/gogh/local.go @@ -161,7 +161,7 @@ func parseProject(ctx Context, root string, fullPath string) (*Project, error) { if len(pathParts) != 3 { return nil, errors.New("not supported project path") } - if err := ValidateHost(ctx, pathParts[0]); err != nil { + if err := SupportedHost(ctx, pathParts[0]); err != nil { return nil, err } if err := ValidateOwner(pathParts[1]); err != nil { diff --git a/gogh/repo.go b/gogh/repo.go index 9354376a..78c0ebdf 100644 --- a/gogh/repo.go +++ b/gogh/repo.go @@ -37,11 +37,11 @@ func ParseRepo(rawRepo string) (*Repo, error) { // CheckRepoHost that repo is in supported host func CheckRepoHost(ctx Context, repo *Repo) error { - return ValidateHost(ctx, repo.Host(ctx)) + return SupportedHost(ctx, repo.Host(ctx)) } -// ValidateHost that repo is in supported host -func ValidateHost(ctx Context, host string) error { +// SupportedHost that repo is in supported host +func SupportedHost(ctx Context, host string) error { if host == ctx.GitHubHost() { return nil } @@ -54,30 +54,6 @@ func ValidateHost(ctx Context, host string) error { var hasSchemePattern = regexp.MustCompile("^[^:]+://") var scpLikeURLPattern = regexp.MustCompile("^([^@]+@)?([^:]+):/?(.+)$") -var invalidNameRegexp = regexp.MustCompile(`[^\w\-\.]`) - -func ValidateName(name string) error { - if name == "." || name == ".." { - return errors.New("'.' or '..' is reserved name") - } - if name == "" { - return errors.New("empty project name") - } - if invalidNameRegexp.MatchString(name) { - return errors.New("project name may only contain alphanumeric characters, dots or hyphens") - } - return nil -} - -var validOwnerRegexp = regexp.MustCompile(`^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$`) - -func ValidateOwner(owner string) error { - if !validOwnerRegexp.MatchString(owner) { - return errors.New("owner name may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen") - } - return nil -} - // Set text as Repo func (r *Repo) Set(rawRepo string) error { raw := rawRepo diff --git a/command/util.go b/gogh/util.go similarity index 93% rename from command/util.go rename to gogh/util.go index 6b8c0181..9599cfc6 100644 --- a/command/util.go +++ b/gogh/util.go @@ -1,4 +1,4 @@ -package command +package gogh func unique(items []string) (uniq []string) { dups := map[string]struct{}{} diff --git a/gogh/validation.go b/gogh/validation.go new file mode 100644 index 00000000..7badbeeb --- /dev/null +++ b/gogh/validation.go @@ -0,0 +1,62 @@ +package gogh + +import ( + "errors" + "os" + "path/filepath" + "regexp" +) + +var invalidNameRegexp = regexp.MustCompile(`[^\w\-\.]`) + +func ValidateName(name string) error { + if name == "." || name == ".." { + return errors.New("'.' or '..' is reserved name") + } + if name == "" { + return errors.New("empty project name") + } + if invalidNameRegexp.MatchString(name) { + return errors.New("project name may only contain alphanumeric characters, dots or hyphens") + } + return nil +} + +var validOwnerRegexp = regexp.MustCompile(`^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$`) + +func ValidateOwner(owner string) error { + if !validOwnerRegexp.MatchString(owner) { + return errors.New("owner name may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen") + } + return nil +} + +func ValidateRoot(root []string) error { + for i, v := range root { + path := filepath.Clean(v) + _, err := os.Stat(path) + switch { + case err == nil: + root[i], err = filepath.EvalSymlinks(path) + if err != nil { + return err + } + case os.IsNotExist(err): + root[i] = path + default: + return err + } + } + + return nil +} + +func ValidateContext(ctx Context) error { + if err := ValidateRoot(ctx.Root()); err != nil { + return err + } + if err := ValidateOwner(ctx.GitHubUser()); err != nil { + return err + } + return nil +} diff --git a/internal/mainutil/mainutil.go b/internal/mainutil/mainutil.go index cd7af0ba..46e947cf 100644 --- a/internal/mainutil/mainutil.go +++ b/internal/mainutil/mainutil.go @@ -5,19 +5,18 @@ import ( "github.com/alecthomas/kingpin" "github.com/comail/colog" - "github.com/kyoh86/gogh/command" "github.com/kyoh86/gogh/gogh" "github.com/kyoh86/xdg" ) -func SetConfigFlag(cmd *kingpin.CmdClause, configFile *string) { +func setConfigFlag(cmd *kingpin.CmdClause, configFile *string) { cmd.Flag("config", "configuration file"). Default(filepath.Join(xdg.CacheHome(), "gogh", "config.toml")). Envar("GOGH_CONFIG"). StringVar(configFile) } -func InitLog(ctx gogh.Context) error { +func initLog(ctx gogh.Context) error { lvl, err := colog.ParseLevel(ctx.LogLevel()) if err != nil { return err @@ -29,22 +28,48 @@ func InitLog(ctx gogh.Context) error { return nil } +func currentConfig(configFile string) (*gogh.Config, *gogh.Config, error) { + fileConfig, err := gogh.LoadFileConfig(configFile) + if err != nil { + return nil, nil, err + } + envarConfig, err := gogh.GetEnvarConfig() + if err != nil { + return nil, nil, err + } + + return fileConfig, gogh.MergeConfig(gogh.DefaultConfig(), fileConfig, envarConfig), nil +} + func WrapCommand(cmd *kingpin.CmdClause, f func(gogh.Context) error) (string, func() error) { var configFile string - SetConfigFlag(cmd, &configFile) + setConfigFlag(cmd, &configFile) return cmd.FullCommand(), func() error { - fileConfig, err := command.LoadFileConfig(configFile) + _, config, err := currentConfig(configFile) if err != nil { return err } - envarConfig, err := command.GetEnvarConfig() + + if err := initLog(config); err != nil { + return err + } + return f(config) + } +} + +func WrapConfigurableCommand(cmd *kingpin.CmdClause, f func(gogh.Context, *gogh.Config) error) (string, func() error) { + var configFile string + setConfigFlag(cmd, &configFile) + return cmd.FullCommand(), func() error { + fileConfig, config, err := currentConfig(configFile) if err != nil { return err } - ctx := command.MergeConfig(command.DefaultConfig(), fileConfig, envarConfig) + if err := initLog(config); err != nil { + return err + } - InitLog(&ctx) - return f(&ctx) + return f(config, fileConfig) } } diff --git a/main.go b/main.go index 0ff175c9..d16bf3d2 100644 --- a/main.go +++ b/main.go @@ -72,35 +72,22 @@ func configGetAll(app *kingpin.Application) (string, func() error) { func configSet(app *kingpin.Application) (string, func() error) { var ( - name string - value string - configFile string + name string + value string ) cmd := app.GetCommand("config").Command("set", "set an option") cmd.Arg("name", "option name").Required().StringVar(&name) cmd.Arg("value", "option value").Required().StringVar(&value) - mainutil.SetConfigFlag(cmd, &configFile) - - return cmd.FullCommand(), func() error { - fileConfig, err := command.LoadFileConfig(configFile) - if err != nil { - return err - } - envarConfig, err := command.GetEnvarConfig() - if err != nil { - return err - } - ctx := command.MergeConfig(command.DefaultConfig(), fileConfig, envarConfig) - mainutil.InitLog(&ctx) - config, err := command.ConfigSet(&fileConfig, name, value) + return mainutil.WrapConfigurableCommand(cmd, func(ctx gogh.Context, config *gogh.Config) error { + config, err := command.ConfigSet(config, name, value) if err != nil { return err } _ = config //TODO: save config return nil - } + }) } func get(app *kingpin.Application) (string, func() error) { From af6c1c0422f57fa955c927c0c8ff827a34c4cedd Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Tue, 26 Mar 2019 09:16:42 +0900 Subject: [PATCH 06/30] go mod tidy --- go.mod | 4 ---- go.sum | 14 -------------- 2 files changed, 18 deletions(-) diff --git a/go.mod b/go.mod index ef57687f..a121b1cd 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ require ( github.com/alecthomas/kingpin v0.0.0-20190313214811-7613e5d4efd9 github.com/atotto/clipboard v0.1.1 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect - github.com/caarlos0/env v0.0.0-20190317132158-240f8632d3d1 github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c github.com/github/hub v2.10.0+incompatible github.com/google/go-github/v24 v24.0.1 @@ -17,12 +16,9 @@ require ( github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect github.com/mitchellh/go-homedir v1.0.0 // indirect - github.com/ogier/pflag v0.0.1 // indirect github.com/pelletier/go-toml v1.2.0 github.com/pkg/errors v0.8.0 - github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 - github.com/wacul/ptr v0.0.0-20190222093950-93c3eb3ee7ea golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect diff --git a/go.sum b/go.sum index f4e2ed8c..44216708 100644 --- a/go.sum +++ b/go.sum @@ -10,19 +10,11 @@ github.com/atotto/clipboard v0.1.1 h1:WSoEbAS70E5gw8FbiqFlp69MGsB6dUb4l+0AGGLiVG github.com/atotto/clipboard v0.1.1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/caarlos0/env v0.0.0-20190308142353-9a320ffe30f7 h1:1MK6c94mutWG2rrxhEYpmWk4lZ/U4Tk08DbtPLiGAP8= -github.com/caarlos0/env v0.0.0-20190308142353-9a320ffe30f7/go.mod h1:ARBGf9nGpjcuNQpQLGEsAk8aYO5esFYfNtiKJGx2ioo= -github.com/caarlos0/env v0.0.0-20190317132158-240f8632d3d1 h1:GaPDk7YlXZxu1gXk1jSuGE79IAf4CYIAPN4+/gfQUV0= -github.com/caarlos0/env v0.0.0-20190317132158-240f8632d3d1/go.mod h1:ARBGf9nGpjcuNQpQLGEsAk8aYO5esFYfNtiKJGx2ioo= -github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= -github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c h1:bzYQ6WpR+t35/y19HUkolcg7SYeWZ15IclC9Z4naGHI= github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c/go.mod h1:1WwgAwMKQLYG5I2FBhpVx94YTOAuB2W59IZ7REjSE6Y= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/github/hub v2.6.0+incompatible h1:dnPzCgdKQn7MPbQ1FDdVIyc9J+9RjS1wc2hkPW5QJq8= -github.com/github/hub v2.6.0+incompatible/go.mod h1:zQrzJEdze2hfWJDgktd/L6sROjAdCThFrzjbxw4keTs= github.com/github/hub v2.10.0+incompatible h1:gK3M/y1ZD/Mc0ytSDykzL8C9q8k7pVTTiEa+1SanJZk= github.com/github/hub v2.10.0+incompatible/go.mod h1:zQrzJEdze2hfWJDgktd/L6sROjAdCThFrzjbxw4keTs= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= @@ -52,8 +44,6 @@ github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= -github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -61,13 +51,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 h1:Ko2LQMrRU+Oy/+EDBwX7eZ2jp3C47eDBB8EIhKTun+I= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/wacul/ptr v0.0.0-20190222093950-93c3eb3ee7ea h1:Dixnoi9TyOD0CJZRVtPkYXgzLmwufCNaO/CD19uJMvQ= -github.com/wacul/ptr v0.0.0-20190222093950-93c3eb3ee7ea/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From c7e17c5b6fe4dfdcc0cb674b4babe1fbbb27e582 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Tue, 26 Mar 2019 09:18:56 +0900 Subject: [PATCH 07/30] validate config before run --- internal/mainutil/mainutil.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/mainutil/mainutil.go b/internal/mainutil/mainutil.go index 46e947cf..9016a1af 100644 --- a/internal/mainutil/mainutil.go +++ b/internal/mainutil/mainutil.go @@ -37,8 +37,11 @@ func currentConfig(configFile string) (*gogh.Config, *gogh.Config, error) { if err != nil { return nil, nil, err } - - return fileConfig, gogh.MergeConfig(gogh.DefaultConfig(), fileConfig, envarConfig), nil + config := gogh.MergeConfig(gogh.DefaultConfig(), fileConfig, envarConfig) + if err := gogh.ValidateContext(config); err != nil { + return nil, nil, err + } + return fileConfig, config, nil } func WrapCommand(cmd *kingpin.CmdClause, f func(gogh.Context) error) (string, func() error) { From a546c9b05586a532b37921bfbc3f0e8ee9fdc1e9 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Wed, 27 Mar 2019 01:07:51 +0900 Subject: [PATCH 08/30] cover test --- gogh/config.go | 13 ++--- gogh/config_test.go | 101 +++++++++++++++++++++++++++++++--- gogh/context.go | 43 --------------- gogh/context_test.go | 49 +++++++++++++++++ gogh/validation.go | 3 + gogh/validation_test.go | 44 +++++++++++++++ internal/mainutil/mainutil.go | 15 ++++- 7 files changed, 205 insertions(+), 63 deletions(-) create mode 100644 gogh/context_test.go create mode 100644 gogh/validation_test.go diff --git a/gogh/config.go b/gogh/config.go index d85cd62b..398cc4b7 100644 --- a/gogh/config.go +++ b/gogh/config.go @@ -109,15 +109,10 @@ func DefaultConfig() *Config { return &defaultConfig } -func LoadFileConfig(filename string) (config *Config, err error) { - file, err := os.Open(filename) - switch { - case err == nil: - defer file.Close() - config = &Config{} - err = toml.NewDecoder(file).Decode(config) - case os.IsNotExist(err): - err = nil +func LoadConfig(r io.Reader) (config *Config, err error) { + config = &Config{} + if err := toml.NewDecoder(r).Decode(config); err != nil { + return nil, err } config.VRoot = unique(config.VRoot) return diff --git a/gogh/config_test.go b/gogh/config_test.go index a18fedf9..339f0b89 100644 --- a/gogh/config_test.go +++ b/gogh/config_test.go @@ -1,6 +1,7 @@ package gogh import ( + "bytes" "os" "testing" @@ -16,12 +17,65 @@ func TestConfig(t *testing.T) { } } - t.Run("get context without roots", func(t *testing.T) { + t.Run("merging priority", func(t *testing.T) { resetEnv(t) + require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) + + cfg1, err := GetEnvarConfig() + require.NoError(t, err) + + t.Run("full overwritten config", func(t *testing.T) { + require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx2")) + require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx2")) + require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh87")) + require.NoError(t, os.Setenv(envGoghLogLevel, "debug")) + require.NoError(t, os.Setenv(envGoghRoot, "/baz:/bux")) + + cfg2, err := GetEnvarConfig() + require.NoError(t, err) + + cfg := MergeConfig(cfg2, cfg1) // prior after config + assert.Equal(t, "tokenx1", cfg.GitHubToken()) + assert.Equal(t, "hostx1", cfg.GitHubHost()) + assert.Equal(t, "kyoh86", cfg.GitHubUser()) + assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) + assert.Equal(t, "/foo", cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) + }) + + t.Run("no overwritten config", func(t *testing.T) { + resetEnv(t) + + cfg2, err := GetEnvarConfig() + require.NoError(t, err) + + cfg := MergeConfig(cfg1, cfg2) // prior after config + assert.Equal(t, "tokenx1", cfg.GitHubToken()) + assert.Equal(t, "hostx1", cfg.GitHubHost()) + assert.Equal(t, "kyoh86", cfg.GitHubUser()) + assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) + assert.Equal(t, "/foo", cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) + }) + + }) + + t.Run("get context from envar", func(t *testing.T) { + resetEnv(t) + require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) + require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) + require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) + require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) cfg, err := GetEnvarConfig() require.NoError(t, err) @@ -29,23 +83,52 @@ func TestConfig(t *testing.T) { assert.Equal(t, "hostx1", cfg.GitHubHost()) assert.Equal(t, "kyoh86", cfg.GitHubUser()) assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) + assert.Equal(t, "/foo", cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) }) - t.Run("expect to get invalid user name", func(t *testing.T) { + t.Run("get context from config", func(t *testing.T) { resetEnv(t) - require.NoError(t, os.Setenv(envGoghGitHubUser, "-kyoh88")) - cfg, err := GetEnvarConfig() + cfg, err := LoadConfig(bytes.NewBufferString(` +loglevel = "trace" +root = ["/foo", "/bar"] +[github] +token = "tokenx1" +user = "kyoh86" +host = "hostx1" +`)) require.NoError(t, err) - require.NotNil(t, ValidateContext(cfg)) + assert.Equal(t, "tokenx1", cfg.GitHubToken()) + assert.Equal(t, "hostx1", cfg.GitHubHost()) + assert.Equal(t, "kyoh86", cfg.GitHubUser()) + assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) + assert.Equal(t, "/foo", cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) }) - t.Run("get root paths", func(t *testing.T) { + t.Run("get default context", func(t *testing.T) { resetEnv(t) - require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) + cfg := DefaultConfig() + assert.Equal(t, "", cfg.GitHubToken()) + assert.Equal(t, "github.com", cfg.GitHubHost()) + assert.Equal(t, "", cfg.GitHubUser()) + assert.Equal(t, "warn", cfg.LogLevel()) + assert.NotEmpty(t, cfg.Root()) + assert.NotEmpty(t, cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) + }) + + t.Run("expect to get invalid user name", func(t *testing.T) { + resetEnv(t) + require.NoError(t, os.Setenv(envGoghGitHubUser, "-kyoh88")) cfg, err := GetEnvarConfig() require.NoError(t, err) - assert.NoError(t, err) - assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) + require.NotNil(t, ValidateContext(cfg)) }) t.Run("expects roots are not duplicated", func(t *testing.T) { diff --git a/gogh/context.go b/gogh/context.go index 42ff4a19..51f533dc 100644 --- a/gogh/context.go +++ b/gogh/context.go @@ -17,46 +17,3 @@ type Context interface { Root() []string PrimaryRoot() string } - -type implContext struct { - context.Context - stdout io.Writer - stderr io.Writer - gitHubUser string - gitHubToken string - gitHubHost string - logLevel string - root []string -} - -func (c *implContext) Stdout() io.Writer { - return c.stdout -} - -func (c *implContext) Stderr() io.Writer { - return c.stderr -} - -func (c *implContext) GitHubUser() string { - return c.gitHubUser -} - -func (c *implContext) GitHubToken() string { - return c.gitHubToken -} - -func (c *implContext) GitHubHost() string { - return c.gitHubHost -} - -func (c *implContext) LogLevel() string { - return c.logLevel -} - -func (c *implContext) Root() []string { - return c.root -} - -func (c *implContext) PrimaryRoot() string { - return c.root[0] -} diff --git a/gogh/context_test.go b/gogh/context_test.go new file mode 100644 index 00000000..82ea6d88 --- /dev/null +++ b/gogh/context_test.go @@ -0,0 +1,49 @@ +package gogh + +import ( + "context" + "io" +) + +type implContext struct { + context.Context + stdout io.Writer + stderr io.Writer + gitHubUser string + gitHubToken string + gitHubHost string + logLevel string + root []string +} + +func (c *implContext) Stdout() io.Writer { + return c.stdout +} + +func (c *implContext) Stderr() io.Writer { + return c.stderr +} + +func (c *implContext) GitHubUser() string { + return c.gitHubUser +} + +func (c *implContext) GitHubToken() string { + return c.gitHubToken +} + +func (c *implContext) GitHubHost() string { + return c.gitHubHost +} + +func (c *implContext) LogLevel() string { + return c.logLevel +} + +func (c *implContext) Root() []string { + return c.root +} + +func (c *implContext) PrimaryRoot() string { + return c.root[0] +} diff --git a/gogh/validation.go b/gogh/validation.go index 7badbeeb..0de2e367 100644 --- a/gogh/validation.go +++ b/gogh/validation.go @@ -47,6 +47,9 @@ func ValidateRoot(root []string) error { return err } } + if len(root) == 0 { + return errors.New("no root") + } return nil } diff --git a/gogh/validation_test.go b/gogh/validation_test.go new file mode 100644 index 00000000..0a11a237 --- /dev/null +++ b/gogh/validation_test.go @@ -0,0 +1,44 @@ +package gogh + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateName(t *testing.T) { + assert.EqualError(t, ValidateName(""), "empty project name", "empty project name is invalid") + assert.EqualError(t, ValidateName("."), "'.' or '..' is reserved name", "'dot' conflicts with 'current directory'") + assert.EqualError(t, ValidateName(".."), "'.' or '..' is reserved name", "'dot' conflicts with 'parent directory'") + assert.EqualError(t, ValidateName("kyoh86/gogh"), "project name may only contain alphanumeric characters, dots or hyphens", "slashes must not be contained in project name") + assert.NoError(t, ValidateName("----..--.."), "hyphens and dots are usable in project name") +} + +func TestValidateOwner(t *testing.T) { + expect := "owner name may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen" + assert.EqualError(t, ValidateOwner(""), expect, "fail when empty owner is given") + assert.EqualError(t, ValidateOwner("kyoh_86"), expect, "fail when owner name contains invalid charactor") + assert.EqualError(t, ValidateOwner("-kyoh86"), expect, "fail when owner name starts with hyphen") + assert.EqualError(t, ValidateOwner("kyoh86-"), expect, "fail when owner name ends with hyphen") + assert.NoError(t, ValidateOwner("kyoh86"), "success") +} + +func TestValidateRoot(t *testing.T) { + tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") + require.NoError(t, err) + assert.EqualError(t, ValidateRoot([]string{}), "no root", "fail when no path in root") + assert.NoError(t, ValidateRoot([]string{"/path/to/not/existing", tmp})) +} + +func TestValidateContext(t *testing.T) { + ctx := &Config{ + VRoot: RootConfig{"/path/to/not/existing"}, + } + ctx.GitHub.User = "" + assert.Error(t, ValidateContext(ctx), "fail when empty owner is given") + ctx.GitHub.User = "kyoh86" + assert.NoError(t, ValidateContext(ctx), "success") +} diff --git a/internal/mainutil/mainutil.go b/internal/mainutil/mainutil.go index 9016a1af..519a8e6a 100644 --- a/internal/mainutil/mainutil.go +++ b/internal/mainutil/mainutil.go @@ -1,6 +1,7 @@ package mainutil import ( + "os" "path/filepath" "github.com/alecthomas/kingpin" @@ -29,8 +30,18 @@ func initLog(ctx gogh.Context) error { } func currentConfig(configFile string) (*gogh.Config, *gogh.Config, error) { - fileConfig, err := gogh.LoadFileConfig(configFile) - if err != nil { + var fileConfig *gogh.Config + file, err := os.Open(configFile) + switch { + case err == nil: + defer file.Close() + fileConfig, err = gogh.LoadConfig(file) + if err != nil { + return nil, nil, err + } + case os.IsNotExist(err): + fileConfig = &gogh.Config{} + default: return nil, nil, err } envarConfig, err := gogh.GetEnvarConfig() From 57374df3dca90e9e46636af51bac67a207f06df4 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Wed, 27 Mar 2019 01:15:20 +0900 Subject: [PATCH 09/30] Cover Formatter --- gogh/formatter_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gogh/formatter_test.go b/gogh/formatter_test.go index 60066371..f0355ba6 100644 --- a/gogh/formatter_test.go +++ b/gogh/formatter_test.go @@ -31,6 +31,7 @@ func TestFormatter(t *testing.T) { require.NoError(t, err) formatter.Add(project1) formatter.Add(project2) + assert.Equal(t, 2, formatter.Len()) var buf bytes.Buffer require.NoError(t, formatter.PrintAll(&buf, ":")) assert.Equal(t, `github.com/kyoh86/foo:github.com/kyoh86/bar:`, buf.String()) @@ -53,6 +54,7 @@ func TestFormatter(t *testing.T) { require.NoError(t, err) formatter.Add(project1) formatter.Add(project2) + assert.Equal(t, 2, formatter.Len()) var buf bytes.Buffer require.NoError(t, formatter.PrintAll(&buf, ":")) assert.Equal(t, `/go/src/github.com/kyoh86/foo:/go/src/github.com/kyoh86/bar:`, buf.String()) @@ -75,6 +77,7 @@ func TestFormatter(t *testing.T) { require.NoError(t, err) formatter.Add(project1) formatter.Add(project2) + assert.Equal(t, 2, formatter.Len()) var buf bytes.Buffer require.NoError(t, formatter.PrintAll(&buf, ":")) assert.Equal(t, `https://github.com/kyoh86/foo:https://github.com/kyoh86/bar:`, buf.String()) @@ -109,6 +112,7 @@ func TestFormatter(t *testing.T) { formatter.Add(project4) formatter.Add(project5) formatter.Add(project6) + assert.Equal(t, 6, formatter.Len()) var buf bytes.Buffer require.NoError(t, formatter.PrintAll(&buf, ":")) assert.Equal(t, `foo:github.com/kyoh86/bar:kyoh87/bar:example.com/kyoh86/bar:/go/src/github.com/kyoh86/baz:/foo/github.com/kyoh86/baz:`, buf.String()) From 93281873ff77828785484d03034016dd480e13df Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Wed, 27 Mar 2019 09:08:32 +0900 Subject: [PATCH 10/30] Configurable commands --- command/config.go | 35 ++++++- command/repos.go | 2 +- config/accessor.go | 166 ++++++++++++++++++++++++++++++ {gogh => config}/config.go | 9 +- {gogh => config}/config_test.go | 10 +- gogh/util.go | 13 --- gogh/validation.go | 45 +++++--- gogh/validation_test.go | 15 +-- internal/mainutil/mainutil.go | 21 ++-- internal/util/util.go | 14 +++ main.go | 42 +++++--- {gogh/remote => remote}/client.go | 0 {gogh/remote => remote}/repos.go | 0 13 files changed, 294 insertions(+), 78 deletions(-) create mode 100644 config/accessor.go rename {gogh => config}/config.go (93%) rename {gogh => config}/config_test.go (94%) create mode 100644 internal/util/util.go rename {gogh/remote => remote}/client.go (100%) rename {gogh/remote => remote}/repos.go (100%) diff --git a/command/config.go b/command/config.go index b4cf4a73..402082a7 100644 --- a/command/config.go +++ b/command/config.go @@ -1,17 +1,44 @@ package command import ( + "fmt" + + "github.com/kyoh86/gogh/config" "github.com/kyoh86/gogh/gogh" ) -func ConfigGet(ctx gogh.Context, optionName string) error { +func ConfigGetAll(ctx gogh.Context) error { + cfg := config.DefaultConfigAccessor + for _, name := range cfg.OptionNames() { + acc, _ := cfg.Accessor(name) // ignore error: cfg.OptionNames covers all accessor + value := acc.Get(ctx) + fmt.Printf("%s = %s\n", name, value) + } return nil } -func ConfigGetAll(ctx gogh.Context) error { +func ConfigGet(ctx gogh.Context, optionName string) error { + acc, err := config.DefaultConfigAccessor.Accessor(optionName) + if err != nil { + return err + } + value := acc.Get(ctx) + fmt.Printf("%s = %s\n", optionName, value) return nil } -func ConfigSet(config *gogh.Config, optionName, optionValue string) (*gogh.Config, error) { - return nil, nil +func ConfigSet(cfg *config.Config, optionName, optionValue string) error { + acc, err := config.DefaultConfigAccessor.Accessor(optionName) + if err != nil { + return err + } + return acc.Set(cfg, optionValue) +} + +func ConfigUnset(cfg *config.Config, optionName string) error { + acc, err := config.DefaultConfigAccessor.Accessor(optionName) + if err != nil { + return err + } + return acc.Unset(cfg) } diff --git a/command/repos.go b/command/repos.go index bc25e1b6..4c706818 100644 --- a/command/repos.go +++ b/command/repos.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/kyoh86/gogh/gogh" - "github.com/kyoh86/gogh/gogh/remote" + "github.com/kyoh86/gogh/remote" ) // Repos will show a list of repositories for a user. diff --git a/config/accessor.go b/config/accessor.go new file mode 100644 index 00000000..f0ee3df6 --- /dev/null +++ b/config/accessor.go @@ -0,0 +1,166 @@ +package config + +import ( + "errors" + "strings" + + "github.com/kyoh86/gogh/gogh" + "github.com/kyoh86/gogh/internal/util" +) + +type ConfigAccessor map[string]OptionAccessor + +var ( + DefaultConfigAccessor = NewConfigAccessor() +) + +func (m ConfigAccessor) Accessor(optionName string) (*OptionAccessor, error) { + a, ok := m[optionName] + if !ok { + return nil, InvalidOptionName + } + return &a, nil +} + +func (m ConfigAccessor) OptionNames() []string { + arr := make([]string, 0, len(m)) + for o := range m { + arr = append(arr, o) + } + return arr +} + +type OptionAccessor struct { + optionName string + getter func(ctx gogh.Context) string + setter func(config *Config, value string) error + unsetter func(config *Config) error +} + +func (a OptionAccessor) Get(ctx gogh.Context) string { return a.getter(ctx) } +func (a OptionAccessor) Set(config *Config, value string) error { return a.setter(config, value) } +func (a OptionAccessor) Unset(config *Config) error { return a.unsetter(config) } + +var ( + EmptyValue = errors.New("empty value") + RemoveFromMonoOption = errors.New("removing from mono option") + InvalidOptionName = errors.New("invalid option name") +) + +// TODO: generate + +func NewConfigAccessor() ConfigAccessor { + m := ConfigAccessor{} + for _, a := range []OptionAccessor{ + GitHubUserOptionAccessor, + GitHubTokenOptionAccessor, + GitHubHostOptionAccessor, + LogLevelOptionAccessor, + RootOptionAccessor, + } { + m[a.optionName] = a + } + return m +} + +var ( + GitHubUserOptionAccessor = OptionAccessor{ + optionName: "github.user", + getter: func(ctx gogh.Context) string { + return ctx.GitHubUser() + }, + setter: func(config *Config, value string) error { + if value == "" { + return EmptyValue + } + if err := gogh.ValidateOwner(value); err != nil { + return err + } + config.GitHub.User = value + return nil + }, + unsetter: func(config *Config) error { + config.GitHub.User = "" + return nil + }, + } + + GitHubTokenOptionAccessor = OptionAccessor{ + optionName: "github.token", + getter: func(ctx gogh.Context) string { + return ctx.GitHubToken() + }, + setter: func(config *Config, value string) error { + if value == "" { + return EmptyValue + } + config.GitHub.Token = value + return nil + }, + unsetter: func(config *Config) error { + config.GitHub.Token = "" + return nil + }, + } + + GitHubHostOptionAccessor = OptionAccessor{ + optionName: "github.host", + getter: func(ctx gogh.Context) string { + return ctx.GitHubHost() + }, + setter: func(config *Config, value string) error { + if value == "" { + return EmptyValue + } + config.GitHub.Host = value + return nil + }, + unsetter: func(config *Config) error { + config.GitHub.Host = "" + return nil + }, + } + + LogLevelOptionAccessor = OptionAccessor{ + optionName: "loglevel", + getter: func(ctx gogh.Context) string { + return ctx.LogLevel() + }, + setter: func(config *Config, value string) error { + if value == "" { + return EmptyValue + } + if err := gogh.ValidateLogLevel(value); err != nil { + return err + } + config.VLogLevel = value + return nil + }, + unsetter: func(config *Config) error { + config.VLogLevel = "" + return nil + }, + } + + RootOptionAccessor = OptionAccessor{ + optionName: "root", + getter: func(ctx gogh.Context) string { + return strings.Join(ctx.Root(), "\n") + }, + setter: func(config *Config, value string) error { + if value == "" { + return EmptyValue + } + path, err := gogh.ValidateRoot(value) + if err != nil { + return err + } + config.VRoot = util.UniqueStringArray(append(config.VRoot, path)) + return nil + }, + unsetter: func(config *Config) error { + config.VRoot = nil + return nil + }, + } +) diff --git a/gogh/config.go b/config/config.go similarity index 93% rename from gogh/config.go rename to config/config.go index 398cc4b7..091a46d0 100644 --- a/gogh/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -package gogh +package config import ( "context" @@ -10,6 +10,7 @@ import ( "sync" "github.com/joeshaw/envdecode" + "github.com/kyoh86/gogh/internal/util" "github.com/pelletier/go-toml" ) @@ -104,7 +105,7 @@ func DefaultConfig() *Config { for _, gopath := range gopaths { root = append(root, filepath.Join(gopath, "src")) } - defaultConfig.VRoot = unique(root) + defaultConfig.VRoot = util.UniqueStringArray(root) }) return &defaultConfig } @@ -114,7 +115,7 @@ func LoadConfig(r io.Reader) (config *Config, err error) { if err := toml.NewDecoder(r).Decode(config); err != nil { return nil, err } - config.VRoot = unique(config.VRoot) + config.VRoot = util.UniqueStringArray(config.VRoot) return } @@ -124,7 +125,7 @@ func GetEnvarConfig() (config *Config, err error) { if err == envdecode.ErrNoTargetFieldsAreSet { err = nil } - config.VRoot = unique(config.VRoot) + config.VRoot = util.UniqueStringArray(config.VRoot) return } diff --git a/gogh/config_test.go b/config/config_test.go similarity index 94% rename from gogh/config_test.go rename to config/config_test.go index 339f0b89..ae01e9dc 100644 --- a/gogh/config_test.go +++ b/config/config_test.go @@ -1,4 +1,4 @@ -package gogh +package config import ( "bytes" @@ -123,14 +123,6 @@ host = "hostx1" assert.Equal(t, os.Stdout, cfg.Stdout()) }) - t.Run("expect to get invalid user name", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGoghGitHubUser, "-kyoh88")) - cfg, err := GetEnvarConfig() - require.NoError(t, err) - require.NotNil(t, ValidateContext(cfg)) - }) - t.Run("expects roots are not duplicated", func(t *testing.T) { resetEnv(t) require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar:/bar:/foo")) diff --git a/gogh/util.go b/gogh/util.go index 9599cfc6..72f551ad 100644 --- a/gogh/util.go +++ b/gogh/util.go @@ -1,14 +1 @@ package gogh - -func unique(items []string) (uniq []string) { - dups := map[string]struct{}{} - - for _, item := range items { - if _, ok := dups[item]; ok { - continue - } - dups[item] = struct{}{} - uniq = append(uniq, item) - } - return -} diff --git a/gogh/validation.go b/gogh/validation.go index 0de2e367..7ffc6558 100644 --- a/gogh/validation.go +++ b/gogh/validation.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "regexp" + + "github.com/comail/colog" ) var invalidNameRegexp = regexp.MustCompile(`[^\w\-\.]`) @@ -31,35 +33,48 @@ func ValidateOwner(owner string) error { return nil } -func ValidateRoot(root []string) error { - for i, v := range root { - path := filepath.Clean(v) - _, err := os.Stat(path) - switch { - case err == nil: - root[i], err = filepath.EvalSymlinks(path) - if err != nil { - return err - } - case os.IsNotExist(err): - root[i] = path - default: +func ValidateRoot(root string) (string, error) { + path := filepath.Clean(root) + _, err := os.Stat(path) + switch { + case err == nil: + return filepath.EvalSymlinks(path) + case os.IsNotExist(err): + return path, nil + default: + return "", err + } +} + +func ValidateRoots(roots []string) error { + for i, v := range roots { + r, err := ValidateRoot(v) + if err != nil { return err } + roots[i] = r } - if len(root) == 0 { + if len(roots) == 0 { return errors.New("no root") } return nil } +func ValidateLogLevel(level string) error { + _, err := colog.ParseLevel(level) + return err +} + func ValidateContext(ctx Context) error { - if err := ValidateRoot(ctx.Root()); err != nil { + if err := ValidateRoots(ctx.Root()); err != nil { return err } if err := ValidateOwner(ctx.GitHubUser()); err != nil { return err } + if err := ValidateLogLevel(ctx.LogLevel()); err != nil { + return err + } return nil } diff --git a/gogh/validation_test.go b/gogh/validation_test.go index 0a11a237..d722b046 100644 --- a/gogh/validation_test.go +++ b/gogh/validation_test.go @@ -26,19 +26,20 @@ func TestValidateOwner(t *testing.T) { assert.NoError(t, ValidateOwner("kyoh86"), "success") } -func TestValidateRoot(t *testing.T) { +func TestValidateRoots(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - assert.EqualError(t, ValidateRoot([]string{}), "no root", "fail when no path in root") - assert.NoError(t, ValidateRoot([]string{"/path/to/not/existing", tmp})) + assert.EqualError(t, ValidateRoots([]string{}), "no root", "fail when no path in root") + assert.NoError(t, ValidateRoots([]string{"/path/to/not/existing", tmp})) } func TestValidateContext(t *testing.T) { - ctx := &Config{ - VRoot: RootConfig{"/path/to/not/existing"}, + ctx := &implContext{ + root: []string{"/path/to/not/existing"}, + logLevel: "warn", } - ctx.GitHub.User = "" + ctx.gitHubUser = "" assert.Error(t, ValidateContext(ctx), "fail when empty owner is given") - ctx.GitHub.User = "kyoh86" + ctx.gitHubUser = "kyoh86" assert.NoError(t, ValidateContext(ctx), "success") } diff --git a/internal/mainutil/mainutil.go b/internal/mainutil/mainutil.go index 519a8e6a..650410f6 100644 --- a/internal/mainutil/mainutil.go +++ b/internal/mainutil/mainutil.go @@ -6,6 +6,7 @@ import ( "github.com/alecthomas/kingpin" "github.com/comail/colog" + "github.com/kyoh86/gogh/config" "github.com/kyoh86/gogh/gogh" "github.com/kyoh86/xdg" ) @@ -29,26 +30,26 @@ func initLog(ctx gogh.Context) error { return nil } -func currentConfig(configFile string) (*gogh.Config, *gogh.Config, error) { - var fileConfig *gogh.Config +func currentConfig(configFile string) (*config.Config, *config.Config, error) { + var fileConfig *config.Config file, err := os.Open(configFile) switch { case err == nil: defer file.Close() - fileConfig, err = gogh.LoadConfig(file) + fileConfig, err = config.LoadConfig(file) if err != nil { return nil, nil, err } case os.IsNotExist(err): - fileConfig = &gogh.Config{} + fileConfig = &config.Config{} default: return nil, nil, err } - envarConfig, err := gogh.GetEnvarConfig() + envarConfig, err := config.GetEnvarConfig() if err != nil { return nil, nil, err } - config := gogh.MergeConfig(gogh.DefaultConfig(), fileConfig, envarConfig) + config := config.MergeConfig(config.DefaultConfig(), fileConfig, envarConfig) if err := gogh.ValidateContext(config); err != nil { return nil, nil, err } @@ -71,7 +72,7 @@ func WrapCommand(cmd *kingpin.CmdClause, f func(gogh.Context) error) (string, fu } } -func WrapConfigurableCommand(cmd *kingpin.CmdClause, f func(gogh.Context, *gogh.Config) error) (string, func() error) { +func WrapConfigurableCommand(cmd *kingpin.CmdClause, f func(*config.Config) error) (string, func() error) { var configFile string setConfigFlag(cmd, &configFile) return cmd.FullCommand(), func() error { @@ -84,6 +85,10 @@ func WrapConfigurableCommand(cmd *kingpin.CmdClause, f func(gogh.Context, *gogh. return err } - return f(config, fileConfig) + if err = f(fileConfig); err != nil { + return err + } + //TODO: save fileConfig + return nil } } diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 00000000..5084f493 --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,14 @@ +package util + +func UniqueStringArray(items []string) (uniq []string) { + dups := map[string]struct{}{} + + for _, item := range items { + if _, ok := dups[item]; ok { + continue + } + dups[item] = struct{}{} + uniq = append(uniq, item) + } + return +} diff --git a/main.go b/main.go index d16bf3d2..8abee584 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "github.com/alecthomas/kingpin" "github.com/kyoh86/gogh/command" + "github.com/kyoh86/gogh/config" "github.com/kyoh86/gogh/gogh" "github.com/kyoh86/gogh/internal/mainutil" ) @@ -24,9 +25,10 @@ func main() { cmds := map[string]func() error{} for _, f := range []func(*kingpin.Application) (string, func() error){ - configGet, configGetAll, + configGet, configSet, + configUnset, get, bulk, @@ -50,6 +52,14 @@ func main() { } } +func configGetAll(app *kingpin.Application) (string, func() error) { + cmd := app.GetCommand("config").Command("get-all", "get all options") + + return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { + return command.ConfigGetAll(ctx) + }) +} + func configGet(app *kingpin.Application) (string, func() error) { var ( name string @@ -62,14 +72,6 @@ func configGet(app *kingpin.Application) (string, func() error) { }) } -func configGetAll(app *kingpin.Application) (string, func() error) { - cmd := app.GetCommand("config").Command("get-all", "get all options") - - return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { - return command.ConfigGetAll(ctx) - }) -} - func configSet(app *kingpin.Application) (string, func() error) { var ( name string @@ -79,14 +81,20 @@ func configSet(app *kingpin.Application) (string, func() error) { cmd.Arg("name", "option name").Required().StringVar(&name) cmd.Arg("value", "option value").Required().StringVar(&value) - return mainutil.WrapConfigurableCommand(cmd, func(ctx gogh.Context, config *gogh.Config) error { - config, err := command.ConfigSet(config, name, value) - if err != nil { - return err - } - _ = config - //TODO: save config - return nil + return mainutil.WrapConfigurableCommand(cmd, func(config *config.Config) error { + return command.ConfigSet(config, name, value) + }) +} + +func configUnset(app *kingpin.Application) (string, func() error) { + var ( + name string + ) + cmd := app.GetCommand("config").Command("unset", "unset an option") + cmd.Arg("name", "option name").Required().StringVar(&name) + + return mainutil.WrapConfigurableCommand(cmd, func(config *config.Config) error { + return command.ConfigUnset(config, name) }) } diff --git a/gogh/remote/client.go b/remote/client.go similarity index 100% rename from gogh/remote/client.go rename to remote/client.go diff --git a/gogh/remote/repos.go b/remote/repos.go similarity index 100% rename from gogh/remote/repos.go rename to remote/repos.go From 0c8ed82ff3d021031531de0dcb9ac7193fd17c0b Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Wed, 27 Mar 2019 16:53:02 +0900 Subject: [PATCH 11/30] Use YAML instead of TOML TOML is minor and its golang parser does not survive. --- config/accessor.go | 6 +- config/bool.go | 53 ++++++++++++ config/config.go | 153 ++++++++++------------------------ config/config_test.go | 61 ++++++++++---- config/get.go | 131 +++++++++++++++++++++++++++++ config/string_array.go | 26 ++++++ go.mod | 3 +- go.sum | 1 - gogh/context.go | 1 + internal/mainutil/mainutil.go | 1 + 10 files changed, 305 insertions(+), 131 deletions(-) create mode 100644 config/bool.go create mode 100644 config/get.go create mode 100644 config/string_array.go diff --git a/config/accessor.go b/config/accessor.go index f0ee3df6..82f0bebf 100644 --- a/config/accessor.go +++ b/config/accessor.go @@ -122,7 +122,7 @@ var ( } LogLevelOptionAccessor = OptionAccessor{ - optionName: "loglevel", + optionName: "log.level", getter: func(ctx gogh.Context) string { return ctx.LogLevel() }, @@ -133,11 +133,11 @@ var ( if err := gogh.ValidateLogLevel(value); err != nil { return err } - config.VLogLevel = value + config.Log.Level = value return nil }, unsetter: func(config *Config) error { - config.VLogLevel = "" + config.Log.Level = "" return nil }, } diff --git a/config/bool.go b/config/bool.go new file mode 100644 index 00000000..98a8cfb9 --- /dev/null +++ b/config/bool.go @@ -0,0 +1,53 @@ +package config + +import "strconv" + +type BoolConfig struct { + filled bool + value bool +} + +var ( + TrueConfig = BoolConfig{ + filled: true, + value: true, + } + FalseConfig = BoolConfig{ + filled: true, + value: false, + } +) + +func (c BoolConfig) Bool() bool { + return c.filled && c.value +} + +// Decode implements the interface `envdecode.Decoder` +func (c *BoolConfig) Decode(repl string) error { + parsed, err := strconv.ParseBool(repl) + if err != nil { + return err + } + c.filled = true + c.value = parsed + return nil +} + +// MarshalYAML implements the interface `yaml.Marshaler` +func (c *BoolConfig) MarshalYAML() (interface{}, error) { + if !c.filled { + return nil, nil + } + return c.value, nil +} + +// UnmarshalYAML implements the interface `yaml.Unmarshaler` +func (c *BoolConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + var parsed bool + if err := unmarshal(&parsed); err != nil { + return err + } + c.filled = true + c.value = parsed + return nil +} diff --git a/config/config.go b/config/config.go index 091a46d0..9057f5a8 100644 --- a/config/config.go +++ b/config/config.go @@ -2,24 +2,33 @@ package config import ( "context" - "go/build" "io" + "log" "os" - "path/filepath" - "strings" - "sync" - - "github.com/joeshaw/envdecode" - "github.com/kyoh86/gogh/internal/util" - "github.com/pelletier/go-toml" ) // Config holds configuration file values. type Config struct { context.Context - VLogLevel string `toml:"loglevel,omitempty" env:"GOGH_LOG_LEVEL"` - VRoot RootConfig `toml:"root,omitempty" env:"GOGH_ROOT"` - GitHub GitHubConfig `toml:"github,omitempty"` + Log LogConfig `yaml:"log"` + VRoot StringArrayConfig `yaml:"root,omitempty" env:"GOGH_ROOT"` + GitHub GitHubConfig `yaml:"github,omitempty"` +} + +type LogConfig struct { + Level string `yaml:"level,omitempty" env:"GOGH_LOG_LEVEL"` + Date BoolConfig `yaml:"date,omitempty" env:"GOGH_LOG_DATE"` // the date in the local time zone: 2009/01/23 + Time BoolConfig `yaml:"time,omitempty" env:"GOGH_LOG_TIME"` // the time in the local time zone: 01:23:23 + MicroSeconds BoolConfig `yaml:"microseconds,omitempty" env:"GOGH_LOG_MICROSECONDS"` // microsecond resolution: 01:23:23.123123. assumes Ltime. + LongFile BoolConfig `yaml:"longfile,omitempty" env:"GOGH_LOG_LONGFILE"` // full file name and line number: /a/b/c/d.go:23 + ShortFile BoolConfig `yaml:"shortfile,omitempty" env:"GOGH_LOG_SHORTFILE"` // final file name element and line number: d.go:23. overrides Llongfile + UTC BoolConfig `yaml:"utc,omitempty" env:"GOGH_LOG_UTC"` // if Ldate or Ltime is set, use UTC rather than the local time zone +} + +type GitHubConfig struct { + Token string `yaml:"token,omitempty" env:"GOGH_GITHUB_TOKEN"` + User string `yaml:"user,omitempty" env:"GOGH_GITHUB_USER"` + Host string `yaml:"host,omitempty" env:"GOGH_GITHUB_HOST"` } func (c *Config) Stdout() io.Writer { @@ -43,114 +52,36 @@ func (c *Config) GitHubHost() string { } func (c *Config) LogLevel() string { - return c.VLogLevel + return c.Log.Level } -func (c *Config) Root() []string { - return c.VRoot -} - -func (c *Config) PrimaryRoot() string { - return c.VRoot[0] -} - -type RootConfig []string - -// Decode implements the interface `envdecode.Decoder` -func (r *RootConfig) Decode(repl string) error { - *r = strings.Split(repl, ":") - return nil -} - -type GitHubConfig struct { - Token string `toml:"token,omitempty" env:"GOGH_GITHUB_TOKEN"` - User string `toml:"user,omitempty" env:"GOGH_GITHUB_USER"` - Host string `toml:"host,omitempty" env:"GOGH_GITHUB_HOST"` -} - -var ( - envGoghLogLevel = "GOGH_LOG_LEVEL" - envGoghGitHubUser = "GOGH_GITHUB_USER" - envGoghGitHubToken = "GOGH_GITHUB_TOKEN" - envGoghGitHubHost = "GOGH_GITHUB_HOST" - envGoghRoot = "GOGH_ROOT" - envNames = []string{ - envGoghLogLevel, - envGoghGitHubUser, - envGoghGitHubToken, - envGoghGitHubHost, - envGoghRoot, +func (c *Config) LogFlags() int { + var f int + if c.Log.Date.Bool() { + f |= log.Ldate } -) - -const ( - // DefaultHost is the default host of the GitHub - DefaultHost = "github.com" - DefaultLogLevel = "warn" -) - -var defaultConfig = Config{ - VLogLevel: DefaultLogLevel, - GitHub: GitHubConfig{ - Host: DefaultHost, - }, -} - -var initDefaultConfig sync.Once - -func DefaultConfig() *Config { - initDefaultConfig.Do(func() { - gopaths := filepath.SplitList(build.Default.GOPATH) - root := make([]string, 0, len(gopaths)) - for _, gopath := range gopaths { - root = append(root, filepath.Join(gopath, "src")) - } - defaultConfig.VRoot = util.UniqueStringArray(root) - }) - return &defaultConfig -} - -func LoadConfig(r io.Reader) (config *Config, err error) { - config = &Config{} - if err := toml.NewDecoder(r).Decode(config); err != nil { - return nil, err + if c.Log.Time.Bool() { + f |= log.Ltime } - config.VRoot = util.UniqueStringArray(config.VRoot) - return -} - -func GetEnvarConfig() (config *Config, err error) { - config = &Config{} - err = envdecode.Decode(config) - if err == envdecode.ErrNoTargetFieldsAreSet { - err = nil + if c.Log.MicroSeconds.Bool() { + f |= log.Lmicroseconds } - config.VRoot = util.UniqueStringArray(config.VRoot) - return -} - -func MergeConfig(base *Config, override ...*Config) *Config { - c := base - for _, o := range override { - c.VLogLevel = mergeStringOption(c.VLogLevel, o.VLogLevel) - c.VRoot = mergeStringArrayOption(c.VRoot, o.VRoot) - c.GitHub.Token = mergeStringOption(c.GitHub.Token, o.GitHub.Token) - c.GitHub.User = mergeStringOption(c.GitHub.User, o.GitHub.User) - c.GitHub.Host = mergeStringOption(c.GitHub.Host, o.GitHub.Host) + if c.Log.LongFile.Bool() { + f |= log.Llongfile + } + if c.Log.ShortFile.Bool() { + f |= log.Lshortfile } - return c + if c.Log.UTC.Bool() { + f |= log.LUTC + } + return f } -func mergeStringOption(base, override string) string { - if override != "" { - return override - } - return base +func (c *Config) Root() []string { + return c.VRoot } -func mergeStringArrayOption(base, override []string) []string { - if len(override) > 0 { - return override - } - return base +func (c *Config) PrimaryRoot() string { + return c.VRoot[0] } diff --git a/config/config_test.go b/config/config_test.go index ae01e9dc..5ee47be4 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "bytes" + "log" "os" "testing" @@ -24,6 +25,12 @@ func TestConfig(t *testing.T) { require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) + require.NoError(t, os.Setenv(envGoghLogDate, "1")) + require.NoError(t, os.Setenv(envGoghLogTime, "1")) + require.NoError(t, os.Setenv(envGoghLogMicroSeconds, "1")) + require.NoError(t, os.Setenv(envGoghLogLongFile, "1")) + require.NoError(t, os.Setenv(envGoghLogShortFile, "1")) + require.NoError(t, os.Setenv(envGoghLogUTC, "1")) require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) cfg1, err := GetEnvarConfig() @@ -34,18 +41,25 @@ func TestConfig(t *testing.T) { require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx2")) require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh87")) require.NoError(t, os.Setenv(envGoghLogLevel, "debug")) + require.NoError(t, os.Setenv(envGoghLogDate, "0")) + require.NoError(t, os.Setenv(envGoghLogTime, "0")) + require.NoError(t, os.Setenv(envGoghLogMicroSeconds, "0")) + require.NoError(t, os.Setenv(envGoghLogLongFile, "0")) + require.NoError(t, os.Setenv(envGoghLogShortFile, "0")) + require.NoError(t, os.Setenv(envGoghLogUTC, "0")) require.NoError(t, os.Setenv(envGoghRoot, "/baz:/bux")) cfg2, err := GetEnvarConfig() require.NoError(t, err) - cfg := MergeConfig(cfg2, cfg1) // prior after config - assert.Equal(t, "tokenx1", cfg.GitHubToken()) - assert.Equal(t, "hostx1", cfg.GitHubHost()) - assert.Equal(t, "kyoh86", cfg.GitHubUser()) - assert.Equal(t, "trace", cfg.LogLevel()) - assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) - assert.Equal(t, "/foo", cfg.PrimaryRoot()) + cfg := MergeConfig(cfg1, cfg2) // prior after config + assert.Equal(t, "tokenx2", cfg.GitHubToken()) + assert.Equal(t, "hostx2", cfg.GitHubHost()) + assert.Equal(t, "kyoh87", cfg.GitHubUser()) + assert.Equal(t, "debug", cfg.LogLevel()) + assert.Equal(t, 0, cfg.LogFlags()) + assert.Equal(t, []string{"/baz", "/bux"}, cfg.Root()) + assert.Equal(t, "/baz", cfg.PrimaryRoot()) assert.Equal(t, os.Stderr, cfg.Stderr()) assert.Equal(t, os.Stdout, cfg.Stdout()) }) @@ -61,6 +75,7 @@ func TestConfig(t *testing.T) { assert.Equal(t, "hostx1", cfg.GitHubHost()) assert.Equal(t, "kyoh86", cfg.GitHubUser()) assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile|log.Lshortfile|log.LUTC, cfg.LogFlags()) assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) assert.Equal(t, "/foo", cfg.PrimaryRoot()) assert.Equal(t, os.Stderr, cfg.Stderr()) @@ -75,14 +90,20 @@ func TestConfig(t *testing.T) { require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) + require.NoError(t, os.Setenv(envGoghLogDate, "1")) + require.NoError(t, os.Setenv(envGoghLogTime, "1")) + require.NoError(t, os.Setenv(envGoghLogMicroSeconds, "1")) + require.NoError(t, os.Setenv(envGoghLogLongFile, "1")) + require.NoError(t, os.Setenv(envGoghLogShortFile, "1")) + require.NoError(t, os.Setenv(envGoghLogUTC, "1")) require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) - cfg, err := GetEnvarConfig() require.NoError(t, err) assert.Equal(t, "tokenx1", cfg.GitHubToken()) assert.Equal(t, "hostx1", cfg.GitHubHost()) assert.Equal(t, "kyoh86", cfg.GitHubUser()) assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile|log.Lshortfile|log.LUTC, cfg.LogFlags()) assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) assert.Equal(t, "/foo", cfg.PrimaryRoot()) assert.Equal(t, os.Stderr, cfg.Stderr()) @@ -92,18 +113,30 @@ func TestConfig(t *testing.T) { t.Run("get context from config", func(t *testing.T) { resetEnv(t) cfg, err := LoadConfig(bytes.NewBufferString(` -loglevel = "trace" -root = ["/foo", "/bar"] -[github] -token = "tokenx1" -user = "kyoh86" -host = "hostx1" +root: +- /foo +- /bar + +log: + level: trace + date: true + time: true + microseconds: true + longfile: true + shortfile: true + utc: true + +github: + token: tokenx1 + user: kyoh86 + host: hostx1 `)) require.NoError(t, err) assert.Equal(t, "tokenx1", cfg.GitHubToken()) assert.Equal(t, "hostx1", cfg.GitHubHost()) assert.Equal(t, "kyoh86", cfg.GitHubUser()) assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile|log.Lshortfile|log.LUTC, cfg.LogFlags()) assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) assert.Equal(t, "/foo", cfg.PrimaryRoot()) assert.Equal(t, os.Stderr, cfg.Stderr()) diff --git a/config/get.go b/config/get.go new file mode 100644 index 00000000..4aeae3ed --- /dev/null +++ b/config/get.go @@ -0,0 +1,131 @@ +package config + +import ( + "go/build" + "io" + "path/filepath" + "sync" + + "github.com/joeshaw/envdecode" + "github.com/kyoh86/gogh/internal/util" + yaml "gopkg.in/yaml.v2" +) + +var ( + envGoghLogLevel = "GOGH_LOG_LEVEL" + envGoghLogDate = "GOGH_LOG_DATE" + envGoghLogTime = "GOGH_LOG_TIME" + envGoghLogMicroSeconds = "GOGH_LOG_MICROSECONDS" + envGoghLogLongFile = "GOGH_LOG_LONGFILE" + envGoghLogShortFile = "GOGH_LOG_SHORTFILE" + envGoghLogUTC = "GOGH_LOG_UTC" + envGoghGitHubUser = "GOGH_GITHUB_USER" + envGoghGitHubToken = "GOGH_GITHUB_TOKEN" + envGoghGitHubHost = "GOGH_GITHUB_HOST" + envGoghRoot = "GOGH_ROOT" + envNames = []string{ + envGoghLogLevel, + envGoghLogDate, + envGoghLogTime, + envGoghLogMicroSeconds, + envGoghLogLongFile, + envGoghLogShortFile, + envGoghLogUTC, + envGoghGitHubUser, + envGoghGitHubToken, + envGoghGitHubHost, + envGoghRoot, + } +) + +const ( + // DefaultHost is the default host of the GitHub + DefaultHost = "github.com" + DefaultLogLevel = "warn" +) + +var defaultConfig = Config{ + Log: LogConfig{ + Level: DefaultLogLevel, + Time: TrueConfig, + }, + GitHub: GitHubConfig{ + Host: DefaultHost, + }, +} + +var initDefaultConfig sync.Once + +func DefaultConfig() *Config { + initDefaultConfig.Do(func() { + gopaths := filepath.SplitList(build.Default.GOPATH) + root := make([]string, 0, len(gopaths)) + for _, gopath := range gopaths { + root = append(root, filepath.Join(gopath, "src")) + } + defaultConfig.VRoot = util.UniqueStringArray(root) + }) + return &defaultConfig +} + +func LoadConfig(r io.Reader) (config *Config, err error) { + config = &Config{} + if err := yaml.NewDecoder(r).Decode(config); err != nil { + return nil, err + } + config.VRoot = util.UniqueStringArray(config.VRoot) + return +} + +func GetEnvarConfig() (config *Config, err error) { + config = &Config{} + err = envdecode.Decode(config) + if err == envdecode.ErrNoTargetFieldsAreSet { + err = nil + } + config.VRoot = util.UniqueStringArray(config.VRoot) + return +} + +func MergeConfig(base *Config, override ...*Config) *Config { + c := *base + for _, o := range override { + c.Log.Level = mergeStringOption(c.Log.Level, o.Log.Level) + c.Log.Date = mergeBoolOption(c.Log.Date, o.Log.Date) + c.Log.Time = mergeBoolOption(c.Log.Time, o.Log.Time) + c.Log.MicroSeconds = mergeBoolOption(c.Log.MicroSeconds, o.Log.MicroSeconds) + c.Log.LongFile = mergeBoolOption(c.Log.LongFile, o.Log.LongFile) + c.Log.ShortFile = mergeBoolOption(c.Log.ShortFile, o.Log.ShortFile) + c.Log.UTC = mergeBoolOption(c.Log.UTC, o.Log.UTC) + c.VRoot = mergeStringArrayOption(c.VRoot, o.VRoot) + c.GitHub.Token = mergeStringOption(c.GitHub.Token, o.GitHub.Token) + c.GitHub.User = mergeStringOption(c.GitHub.User, o.GitHub.User) + c.GitHub.Host = mergeStringOption(c.GitHub.Host, o.GitHub.Host) + } + return &c +} + +func mergeBoolOption(base, override BoolConfig) BoolConfig { + switch { + case override.filled: + return override + case base.filled: + return base + default: + return BoolConfig{} + } +} + +func mergeStringOption(base, override string) string { + if override != "" { + return override + } + return base +} + +func mergeStringArrayOption(base, override []string) []string { + if len(override) > 0 { + return override + } + return base +} diff --git a/config/string_array.go b/config/string_array.go new file mode 100644 index 00000000..220ca1f3 --- /dev/null +++ b/config/string_array.go @@ -0,0 +1,26 @@ +package config + +import "strings" + +type StringArrayConfig []string + +// Decode implements the interface `envdecode.Decoder` +func (c *StringArrayConfig) Decode(repl string) error { + *c = strings.Split(repl, ":") + return nil +} + +// MarshalYAML implements the interface `yaml.Marshaler` +func (c *StringArrayConfig) MarshalYAML() (interface{}, error) { + return []string(*c), nil +} + +// UnmarshalYAML implements the interface `yaml.Unmarshaler` +func (c *StringArrayConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + var parsed []string + if err := unmarshal(&parsed); err != nil { + return err + } + *c = parsed + return nil +} diff --git a/go.mod b/go.mod index a121b1cd..43c956f8 100644 --- a/go.mod +++ b/go.mod @@ -16,12 +16,11 @@ require ( github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect github.com/mitchellh/go-homedir v1.0.0 // indirect - github.com/pelletier/go-toml v1.2.0 github.com/pkg/errors v0.8.0 github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gopkg.in/yaml.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.2.1 ) diff --git a/go.sum b/go.sum index 44216708..92175f37 100644 --- a/go.sum +++ b/go.sum @@ -44,7 +44,6 @@ github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/gogh/context.go b/gogh/context.go index 51f533dc..3aa37c54 100644 --- a/gogh/context.go +++ b/gogh/context.go @@ -14,6 +14,7 @@ type Context interface { GitHubToken() string GitHubHost() string LogLevel() string + LogFlags() int // log.Lxxx flags Root() []string PrimaryRoot() string } diff --git a/internal/mainutil/mainutil.go b/internal/mainutil/mainutil.go index 650410f6..c8b2854d 100644 --- a/internal/mainutil/mainutil.go +++ b/internal/mainutil/mainutil.go @@ -25,6 +25,7 @@ func initLog(ctx gogh.Context) error { } colog.Register() colog.SetOutput(ctx.Stderr()) + colog.SetFlags(ctx.LogFlags()) colog.SetMinLevel(lvl) colog.SetDefaultLevel(colog.LError) return nil From 56ceedb58aa1f37bcb41c765fabb992dc0442f7d Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Thu, 28 Mar 2019 15:17:29 +0900 Subject: [PATCH 12/30] Refactor, add test --- command/config.go | 27 ++-- config/accessor.go | 188 +++++++++++++++++------ config/accessor_test.go | 146 ++++++++++++++++++ config/bool.go | 57 +++---- config/bool_test.go | 65 ++++++++ config/config.go | 28 ++-- config/config_test.go | 167 -------------------- config/get.go | 49 +----- config/get_test.go | 136 ++++++++++++++++ config/merge.go | 44 ++++++ config/merge_test.go | 83 ++++++++++ config/{string_array.go => path_list.go} | 14 +- config/path_list_test.go | 54 +++++++ gogh/context.go | 5 + gogh/context_test.go | 30 +++- gogh/formatter_test.go | 3 - gogh/validation_test.go | 1 + internal/mainutil/mainutil.go | 38 +++-- main.go | 22 +-- 19 files changed, 797 insertions(+), 360 deletions(-) create mode 100644 config/accessor_test.go create mode 100644 config/bool_test.go delete mode 100644 config/config_test.go create mode 100644 config/get_test.go create mode 100644 config/merge.go create mode 100644 config/merge_test.go rename config/{string_array.go => path_list.go} (54%) create mode 100644 config/path_list_test.go diff --git a/command/config.go b/command/config.go index 402082a7..65482bde 100644 --- a/command/config.go +++ b/command/config.go @@ -4,41 +4,40 @@ import ( "fmt" "github.com/kyoh86/gogh/config" - "github.com/kyoh86/gogh/gogh" ) -func ConfigGetAll(ctx gogh.Context) error { - cfg := config.DefaultConfigAccessor - for _, name := range cfg.OptionNames() { - acc, _ := cfg.Accessor(name) // ignore error: cfg.OptionNames covers all accessor - value := acc.Get(ctx) +func ConfigGetAll(cfg *config.Config) error { + acc := config.DefaultAccessor + for _, name := range acc.OptionNames() { + opt, _ := acc.Option(name) // ignore error: acc.OptionNames covers all accessor + value := opt.Get(cfg) fmt.Printf("%s = %s\n", name, value) } return nil } -func ConfigGet(ctx gogh.Context, optionName string) error { - acc, err := config.DefaultConfigAccessor.Accessor(optionName) +func ConfigGet(cfg *config.Config, optionName string) error { + opt, err := config.DefaultAccessor.Option(optionName) if err != nil { return err } - value := acc.Get(ctx) + value := opt.Get(cfg) fmt.Printf("%s = %s\n", optionName, value) return nil } -func ConfigSet(cfg *config.Config, optionName, optionValue string) error { - acc, err := config.DefaultConfigAccessor.Accessor(optionName) +func ConfigPut(cfg *config.Config, optionName, optionValue string) error { + opt, err := config.DefaultAccessor.Option(optionName) if err != nil { return err } - return acc.Set(cfg, optionValue) + return opt.Put(cfg, optionValue) } func ConfigUnset(cfg *config.Config, optionName string) error { - acc, err := config.DefaultConfigAccessor.Accessor(optionName) + opt, err := config.DefaultAccessor.Option(optionName) if err != nil { return err } - return acc.Unset(cfg) + return opt.Unset(cfg) } diff --git a/config/accessor.go b/config/accessor.go index 82f0bebf..75dc90bb 100644 --- a/config/accessor.go +++ b/config/accessor.go @@ -2,19 +2,20 @@ package config import ( "errors" + "path/filepath" "strings" "github.com/kyoh86/gogh/gogh" "github.com/kyoh86/gogh/internal/util" ) -type ConfigAccessor map[string]OptionAccessor +type Accessor map[string]OptionAccessor var ( - DefaultConfigAccessor = NewConfigAccessor() + DefaultAccessor = NewAccessor() ) -func (m ConfigAccessor) Accessor(optionName string) (*OptionAccessor, error) { +func (m Accessor) Option(optionName string) (*OptionAccessor, error) { a, ok := m[optionName] if !ok { return nil, InvalidOptionName @@ -22,7 +23,7 @@ func (m ConfigAccessor) Accessor(optionName string) (*OptionAccessor, error) { return &a, nil } -func (m ConfigAccessor) OptionNames() []string { +func (m Accessor) OptionNames() []string { arr := make([]string, 0, len(m)) for o := range m { arr = append(arr, o) @@ -32,30 +33,34 @@ func (m ConfigAccessor) OptionNames() []string { type OptionAccessor struct { optionName string - getter func(ctx gogh.Context) string - setter func(config *Config, value string) error - unsetter func(config *Config) error + getter func(cfg *Config) string + putter func(cfg *Config, value string) error + unsetter func(cfg *Config) error } -func (a OptionAccessor) Get(ctx gogh.Context) string { return a.getter(ctx) } -func (a OptionAccessor) Set(config *Config, value string) error { return a.setter(config, value) } -func (a OptionAccessor) Unset(config *Config) error { return a.unsetter(config) } +func (a OptionAccessor) Get(cfg *Config) string { return a.getter(cfg) } +func (a OptionAccessor) Put(cfg *Config, value string) error { return a.putter(cfg, value) } +func (a OptionAccessor) Unset(cfg *Config) error { return a.unsetter(cfg) } var ( EmptyValue = errors.New("empty value") RemoveFromMonoOption = errors.New("removing from mono option") InvalidOptionName = errors.New("invalid option name") + TokenMustNotSave = errors.New("token must not save") ) -// TODO: generate - -func NewConfigAccessor() ConfigAccessor { - m := ConfigAccessor{} +func NewAccessor() Accessor { + m := Accessor{} for _, a := range []OptionAccessor{ GitHubUserOptionAccessor, GitHubTokenOptionAccessor, GitHubHostOptionAccessor, LogLevelOptionAccessor, + LogDateOptionAccessor, + LogTimeOptionAccessor, + LogLongFileOptionAccessor, + LogShortFileOptionAccessor, + LogUTCOptionAccessor, RootOptionAccessor, } { m[a.optionName] = a @@ -66,100 +71,183 @@ func NewConfigAccessor() ConfigAccessor { var ( GitHubUserOptionAccessor = OptionAccessor{ optionName: "github.user", - getter: func(ctx gogh.Context) string { - return ctx.GitHubUser() + getter: func(cfg *Config) string { + return cfg.GitHubUser() }, - setter: func(config *Config, value string) error { + putter: func(cfg *Config, value string) error { if value == "" { return EmptyValue } if err := gogh.ValidateOwner(value); err != nil { return err } - config.GitHub.User = value + cfg.GitHub.User = value return nil }, - unsetter: func(config *Config) error { - config.GitHub.User = "" + unsetter: func(cfg *Config) error { + cfg.GitHub.User = "" return nil }, } GitHubTokenOptionAccessor = OptionAccessor{ optionName: "github.token", - getter: func(ctx gogh.Context) string { - return ctx.GitHubToken() + getter: func(cfg *Config) string { + return cfg.GitHubToken() }, - setter: func(config *Config, value string) error { - if value == "" { - return EmptyValue - } - config.GitHub.Token = value - return nil + putter: func(cfg *Config, value string) error { + return TokenMustNotSave }, - unsetter: func(config *Config) error { - config.GitHub.Token = "" + unsetter: func(cfg *Config) error { + cfg.GitHub.Token = "" return nil }, } GitHubHostOptionAccessor = OptionAccessor{ optionName: "github.host", - getter: func(ctx gogh.Context) string { - return ctx.GitHubHost() + getter: func(cfg *Config) string { + return cfg.GitHubHost() }, - setter: func(config *Config, value string) error { + putter: func(cfg *Config, value string) error { if value == "" { return EmptyValue } - config.GitHub.Host = value + cfg.GitHub.Host = value return nil }, - unsetter: func(config *Config) error { - config.GitHub.Host = "" + unsetter: func(cfg *Config) error { + cfg.GitHub.Host = "" return nil }, } LogLevelOptionAccessor = OptionAccessor{ optionName: "log.level", - getter: func(ctx gogh.Context) string { - return ctx.LogLevel() + getter: func(cfg *Config) string { + return cfg.LogLevel() }, - setter: func(config *Config, value string) error { + putter: func(cfg *Config, value string) error { if value == "" { return EmptyValue } if err := gogh.ValidateLogLevel(value); err != nil { return err } - config.Log.Level = value + cfg.Log.Level = value + return nil + }, + unsetter: func(cfg *Config) error { + cfg.Log.Level = "" + return nil + }, + } + + LogDateOptionAccessor = OptionAccessor{ + optionName: "log.date", + getter: func(cfg *Config) string { + return cfg.Log.Date.String() + }, + putter: func(cfg *Config, value string) error { + if value == "" { + return EmptyValue + } + return cfg.Log.Date.Decode(value) + }, + unsetter: func(cfg *Config) error { + cfg.Log.Date = EmptyBoolOption + return nil + }, + } + + LogTimeOptionAccessor = OptionAccessor{ + optionName: "log.time", + getter: func(cfg *Config) string { + return cfg.Log.Time.String() + }, + putter: func(cfg *Config, value string) error { + if value == "" { + return EmptyValue + } + return cfg.Log.Time.Decode(value) + }, + unsetter: func(cfg *Config) error { + cfg.Log.Time = EmptyBoolOption + return nil + }, + } + + LogLongFileOptionAccessor = OptionAccessor{ + optionName: "log.longfile", + getter: func(cfg *Config) string { + return cfg.Log.LongFile.String() + }, + putter: func(cfg *Config, value string) error { + if value == "" { + return EmptyValue + } + return cfg.Log.LongFile.Decode(value) + }, + unsetter: func(cfg *Config) error { + cfg.Log.LongFile = EmptyBoolOption + return nil + }, + } + + LogShortFileOptionAccessor = OptionAccessor{ + optionName: "log.shortfile", + getter: func(cfg *Config) string { + return cfg.Log.ShortFile.String() + }, + putter: func(cfg *Config, value string) error { + if value == "" { + return EmptyValue + } + return cfg.Log.ShortFile.Decode(value) + }, + unsetter: func(cfg *Config) error { + cfg.Log.ShortFile = EmptyBoolOption return nil }, - unsetter: func(config *Config) error { - config.Log.Level = "" + } + + LogUTCOptionAccessor = OptionAccessor{ + optionName: "log.utc", + getter: func(cfg *Config) string { + return cfg.Log.UTC.String() + }, + putter: func(cfg *Config, value string) error { + if value == "" { + return EmptyValue + } + return cfg.Log.UTC.Decode(value) + }, + unsetter: func(cfg *Config) error { + cfg.Log.UTC = EmptyBoolOption return nil }, } RootOptionAccessor = OptionAccessor{ optionName: "root", - getter: func(ctx gogh.Context) string { - return strings.Join(ctx.Root(), "\n") + getter: func(cfg *Config) string { + return strings.Join(cfg.Root(), string(filepath.ListSeparator)) }, - setter: func(config *Config, value string) error { + putter: func(cfg *Config, value string) error { if value == "" { return EmptyValue } - path, err := gogh.ValidateRoot(value) - if err != nil { + + list := filepath.SplitList(value) + + if err := gogh.ValidateRoots(list); err != nil { return err } - config.VRoot = util.UniqueStringArray(append(config.VRoot, path)) + cfg.VRoot = util.UniqueStringArray(append(cfg.VRoot, list...)) return nil }, - unsetter: func(config *Config) error { - config.VRoot = nil + unsetter: func(cfg *Config) error { + cfg.VRoot = nil return nil }, } diff --git a/config/accessor_test.go b/config/accessor_test.go new file mode 100644 index 00000000..01e11e2c --- /dev/null +++ b/config/accessor_test.go @@ -0,0 +1,146 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAccessor(t *testing.T) { + t.Run("getting", func(t *testing.T) { + mustOption := func(acc *OptionAccessor, err error) *OptionAccessor { + t.Helper() + require.NoError(t, err) + return acc + } + var cfg Config + cfg.GitHub.Token = "token1" + cfg.GitHub.Host = "hostx1" + cfg.GitHub.User = "kyoh86" + cfg.Log.Level = "trace" + cfg.Log.Date = TrueOption + cfg.Log.Time = FalseOption + cfg.Log.LongFile = TrueOption + cfg.Log.ShortFile = TrueOption + cfg.Log.UTC = TrueOption + cfg.VRoot = []string{"/foo", "/bar"} + + _, err := DefaultAccessor.Option("invalid name") + assert.EqualError(t, err, "invalid option name") + assert.Equal(t, "token1", mustOption(DefaultAccessor.Option("github.token")).Get(&cfg)) + assert.Equal(t, "hostx1", mustOption(DefaultAccessor.Option("github.host")).Get(&cfg)) + assert.Equal(t, "kyoh86", mustOption(DefaultAccessor.Option("github.user")).Get(&cfg)) + assert.Equal(t, "trace", mustOption(DefaultAccessor.Option("log.level")).Get(&cfg)) + assert.Equal(t, "yes", mustOption(DefaultAccessor.Option("log.date")).Get(&cfg)) + assert.Equal(t, "no", mustOption(DefaultAccessor.Option("log.time")).Get(&cfg)) + assert.Equal(t, "yes", mustOption(DefaultAccessor.Option("log.longfile")).Get(&cfg)) + assert.Equal(t, "yes", mustOption(DefaultAccessor.Option("log.shortfile")).Get(&cfg)) + assert.Equal(t, "yes", mustOption(DefaultAccessor.Option("log.utc")).Get(&cfg)) + assert.Equal(t, "/foo:/bar", mustOption(DefaultAccessor.Option("root")).Get(&cfg)) + }) + t.Run("putting", func(t *testing.T) { + mustOption := func(acc *OptionAccessor, err error) *OptionAccessor { + t.Helper() + require.NoError(t, err) + return acc + } + var cfg Config + assert.NoError(t, mustOption(DefaultAccessor.Option("github.host")).Put(&cfg, "hostx1")) + assert.NoError(t, mustOption(DefaultAccessor.Option("github.user")).Put(&cfg, "kyoh86")) + assert.NoError(t, mustOption(DefaultAccessor.Option("log.level")).Put(&cfg, "trace")) + assert.NoError(t, mustOption(DefaultAccessor.Option("log.date")).Put(&cfg, "yes")) + assert.NoError(t, mustOption(DefaultAccessor.Option("log.time")).Put(&cfg, "no")) + assert.NoError(t, mustOption(DefaultAccessor.Option("log.longfile")).Put(&cfg, "yes")) + assert.NoError(t, mustOption(DefaultAccessor.Option("log.shortfile")).Put(&cfg, "yes")) + assert.NoError(t, mustOption(DefaultAccessor.Option("log.utc")).Put(&cfg, "yes")) + assert.NoError(t, mustOption(DefaultAccessor.Option("root")).Put(&cfg, "/foo:/bar")) + + assert.Equal(t, "", cfg.GitHub.Token) + assert.Equal(t, "hostx1", cfg.GitHub.Host) + assert.Equal(t, "kyoh86", cfg.GitHub.User) + assert.Equal(t, "trace", cfg.Log.Level) + assert.Equal(t, TrueOption, cfg.Log.Date) + assert.True(t, cfg.LogDate()) + assert.Equal(t, FalseOption, cfg.Log.Time) + assert.False(t, cfg.LogTime()) + assert.Equal(t, TrueOption, cfg.Log.LongFile) + assert.True(t, cfg.LogLongFile()) + assert.Equal(t, TrueOption, cfg.Log.ShortFile) + assert.True(t, cfg.LogShortFile()) + assert.Equal(t, TrueOption, cfg.Log.UTC) + assert.True(t, cfg.LogUTC()) + assert.Equal(t, PathListOption{"/foo", "/bar"}, cfg.VRoot) + }) + t.Run("putting error", func(t *testing.T) { + mustOption := func(acc *OptionAccessor, err error) *OptionAccessor { + t.Helper() + require.NoError(t, err) + return acc + } + var cfg Config + assert.EqualError(t, mustOption(DefaultAccessor.Option("github.token")).Put(&cfg, "token1"), "token must not save") + + assert.EqualError(t, mustOption(DefaultAccessor.Option("github.host")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(DefaultAccessor.Option("github.user")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(DefaultAccessor.Option("log.level")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(DefaultAccessor.Option("log.date")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(DefaultAccessor.Option("log.time")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(DefaultAccessor.Option("log.longfile")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(DefaultAccessor.Option("log.shortfile")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(DefaultAccessor.Option("log.utc")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(DefaultAccessor.Option("root")).Put(&cfg, ""), "empty value") + + assert.Error(t, mustOption(DefaultAccessor.Option("github.user")).Put(&cfg, "-kyoh86"), "invalid github username") + assert.Error(t, mustOption(DefaultAccessor.Option("log.level")).Put(&cfg, "foobar"), "invalid log level") + assert.Error(t, mustOption(DefaultAccessor.Option("log.date")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(DefaultAccessor.Option("log.time")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(DefaultAccessor.Option("log.longfile")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(DefaultAccessor.Option("log.shortfile")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(DefaultAccessor.Option("log.utc")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(DefaultAccessor.Option("root")).Put(&cfg, "\x00"), "invalid value") + + assert.Equal(t, "", cfg.GitHub.Token) + assert.Equal(t, "", cfg.GitHub.Host) + assert.Equal(t, "", cfg.GitHub.User) + assert.Equal(t, "", cfg.Log.Level) + assert.Equal(t, EmptyBoolOption, cfg.Log.Date) + assert.Equal(t, EmptyBoolOption, cfg.Log.Time) + assert.Equal(t, EmptyBoolOption, cfg.Log.LongFile) + assert.Equal(t, EmptyBoolOption, cfg.Log.ShortFile) + assert.Equal(t, EmptyBoolOption, cfg.Log.UTC) + assert.Empty(t, cfg.VRoot) + + }) + t.Run("unsetting", func(t *testing.T) { + var cfg Config + cfg.GitHub.Token = "token1" + cfg.GitHub.Host = "hostx1" + cfg.GitHub.User = "kyoh86" + cfg.Log.Level = "trace" + cfg.Log.Date = TrueOption + cfg.Log.Time = FalseOption + cfg.Log.LongFile = TrueOption + cfg.Log.ShortFile = TrueOption + cfg.Log.UTC = TrueOption + cfg.VRoot = []string{"/foo", "/bar"} + + _, err := DefaultAccessor.Option("invalid name") + assert.EqualError(t, err, "invalid option name") + for _, name := range DefaultAccessor.OptionNames() { + acc, err := DefaultAccessor.Option(name) + require.NoError(t, err) + assert.NoError(t, acc.Unset(&cfg), name) + } + assert.Equal(t, "", cfg.GitHub.Token) + assert.Equal(t, "", cfg.GitHub.Host) + assert.Equal(t, "", cfg.GitHub.User) + assert.Equal(t, "", cfg.Log.Level) + assert.Equal(t, EmptyBoolOption, cfg.Log.Date) + assert.Equal(t, EmptyBoolOption, cfg.Log.Time) + assert.Equal(t, EmptyBoolOption, cfg.Log.LongFile) + assert.Equal(t, EmptyBoolOption, cfg.Log.ShortFile) + assert.Equal(t, EmptyBoolOption, cfg.Log.UTC) + assert.Empty(t, cfg.VRoot) + }) +} diff --git a/config/bool.go b/config/bool.go index 98a8cfb9..d3509e59 100644 --- a/config/bool.go +++ b/config/bool.go @@ -1,53 +1,46 @@ package config -import "strconv" +import ( + "errors" + "strings" +) -type BoolConfig struct { - filled bool - value bool -} +type BoolOption string var ( - TrueConfig = BoolConfig{ - filled: true, - value: true, - } - FalseConfig = BoolConfig{ - filled: true, - value: false, - } + TrueOption = BoolOption("yes") + FalseOption = BoolOption("no") + EmptyBoolOption = BoolOption("") ) -func (c BoolConfig) Bool() bool { - return c.filled && c.value +func (c BoolOption) String() string { + return string(c) +} + +func (c BoolOption) Bool() bool { + return c == TrueOption } // Decode implements the interface `envdecode.Decoder` -func (c *BoolConfig) Decode(repl string) error { - parsed, err := strconv.ParseBool(repl) - if err != nil { - return err +func (c *BoolOption) Decode(repl string) error { + switch strings.ToLower(repl) { + case "yes", "no", "": + *c = BoolOption(repl) + return nil } - c.filled = true - c.value = parsed - return nil + return errors.New("invalid type") } // MarshalYAML implements the interface `yaml.Marshaler` -func (c *BoolConfig) MarshalYAML() (interface{}, error) { - if !c.filled { - return nil, nil - } - return c.value, nil +func (c BoolOption) MarshalYAML() (interface{}, error) { + return string(c), nil } // UnmarshalYAML implements the interface `yaml.Unmarshaler` -func (c *BoolConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { - var parsed bool +func (c *BoolOption) UnmarshalYAML(unmarshal func(interface{}) error) error { + var parsed string if err := unmarshal(&parsed); err != nil { return err } - c.filled = true - c.value = parsed - return nil + return c.Decode(parsed) } diff --git a/config/bool_test.go b/config/bool_test.go new file mode 100644 index 00000000..288062d2 --- /dev/null +++ b/config/bool_test.go @@ -0,0 +1,65 @@ +package config + +import ( + "bytes" + "os" + "strings" + "testing" + + "github.com/joeshaw/envdecode" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + yaml "gopkg.in/yaml.v2" +) + +func TestBoolOption(t *testing.T) { + assert.True(t, TrueOption.Bool()) + assert.False(t, FalseOption.Bool()) + + type testStruct struct { + Bool BoolOption `env:"BOOL" yaml:"bool,omitempty"` + } + + t.Run("encode to yaml", func(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, yaml.NewEncoder(&buf).Encode(testStruct{})) + assert.Equal(t, "{}", strings.TrimSpace(buf.String())) + + buf.Reset() + require.NoError(t, yaml.NewEncoder(&buf).Encode(testStruct{Bool: FalseOption})) + assert.Equal(t, `bool: "no"`, strings.TrimSpace(buf.String())) + + buf.Reset() + require.NoError(t, yaml.NewEncoder(&buf).Encode(testStruct{Bool: TrueOption})) + assert.Equal(t, `bool: "yes"`, strings.TrimSpace(buf.String())) + }) + t.Run("decode from YAML", func(t *testing.T) { + var testValue testStruct + require.NoError(t, yaml.Unmarshal([]byte(`{}`), &testValue)) + assert.Equal(t, EmptyBoolOption, testValue.Bool) + + require.NoError(t, yaml.Unmarshal([]byte(`bool: no`), &testValue)) + assert.Equal(t, FalseOption, testValue.Bool) + + require.NoError(t, yaml.Unmarshal([]byte(`bool: yes`), &testValue)) + assert.Equal(t, TrueOption, testValue.Bool) + + require.NoError(t, yaml.Unmarshal([]byte(`bool: ""`), &testValue)) + assert.Equal(t, EmptyBoolOption, testValue.Bool) + + assert.Error(t, yaml.Unmarshal([]byte(`bool: invalid`), &testValue)) + }) + t.Run("get from envar", func(t *testing.T) { + var testValue testStruct + resetEnv(t) + require.NoError(t, os.Setenv("BOOL", "no")) + require.NoError(t, envdecode.Decode(&testValue)) + assert.Equal(t, FalseOption, testValue.Bool) + + testValue = testStruct{} + resetEnv(t) + require.NoError(t, os.Setenv("BOOL", "yes")) + require.NoError(t, envdecode.Decode(&testValue)) + assert.Equal(t, TrueOption, testValue.Bool) + }) +} diff --git a/config/config.go b/config/config.go index 9057f5a8..35150a25 100644 --- a/config/config.go +++ b/config/config.go @@ -9,24 +9,24 @@ import ( // Config holds configuration file values. type Config struct { - context.Context - Log LogConfig `yaml:"log"` - VRoot StringArrayConfig `yaml:"root,omitempty" env:"GOGH_ROOT"` - GitHub GitHubConfig `yaml:"github,omitempty"` + context.Context `yaml:"-"` + Log LogConfig `yaml:"log,omitempty"` + VRoot PathListOption `yaml:"root,omitempty" env:"GOGH_ROOT"` + GitHub GitHubConfig `yaml:"github,omitempty"` } type LogConfig struct { Level string `yaml:"level,omitempty" env:"GOGH_LOG_LEVEL"` - Date BoolConfig `yaml:"date,omitempty" env:"GOGH_LOG_DATE"` // the date in the local time zone: 2009/01/23 - Time BoolConfig `yaml:"time,omitempty" env:"GOGH_LOG_TIME"` // the time in the local time zone: 01:23:23 - MicroSeconds BoolConfig `yaml:"microseconds,omitempty" env:"GOGH_LOG_MICROSECONDS"` // microsecond resolution: 01:23:23.123123. assumes Ltime. - LongFile BoolConfig `yaml:"longfile,omitempty" env:"GOGH_LOG_LONGFILE"` // full file name and line number: /a/b/c/d.go:23 - ShortFile BoolConfig `yaml:"shortfile,omitempty" env:"GOGH_LOG_SHORTFILE"` // final file name element and line number: d.go:23. overrides Llongfile - UTC BoolConfig `yaml:"utc,omitempty" env:"GOGH_LOG_UTC"` // if Ldate or Ltime is set, use UTC rather than the local time zone + Date BoolOption `yaml:"date" env:"GOGH_LOG_DATE"` // the date in the local time zone: 2009/01/23 + Time BoolOption `yaml:"time" env:"GOGH_LOG_TIME"` // the time in the local time zone: 01:23:23 + MicroSeconds BoolOption `yaml:"microseconds" env:"GOGH_LOG_MICROSECONDS"` // microsecond resolution: 01:23:23.123123. assumes Ltime. + LongFile BoolOption `yaml:"longfile" env:"GOGH_LOG_LONGFILE"` // full file name and line number: /a/b/c/d.go:23 + ShortFile BoolOption `yaml:"shortfile" env:"GOGH_LOG_SHORTFILE"` // final file name element and line number: d.go:23. overrides Llongfile + UTC BoolOption `yaml:"utc" env:"GOGH_LOG_UTC"` // if Ldate or Ltime is set, use UTC rather than the local time zone } type GitHubConfig struct { - Token string `yaml:"token,omitempty" env:"GOGH_GITHUB_TOKEN"` + Token string `yaml:"-" env:"GOGH_GITHUB_TOKEN"` User string `yaml:"user,omitempty" env:"GOGH_GITHUB_USER"` Host string `yaml:"host,omitempty" env:"GOGH_GITHUB_HOST"` } @@ -78,6 +78,12 @@ func (c *Config) LogFlags() int { return f } +func (c *Config) LogDate() bool { return c.Log.Date.Bool() } +func (c *Config) LogTime() bool { return c.Log.Time.Bool() } +func (c *Config) LogLongFile() bool { return c.Log.LongFile.Bool() } +func (c *Config) LogShortFile() bool { return c.Log.ShortFile.Bool() } +func (c *Config) LogUTC() bool { return c.Log.UTC.Bool() } + func (c *Config) Root() []string { return c.VRoot } diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index 5ee47be4..00000000 --- a/config/config_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package config - -import ( - "bytes" - "log" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConfig(t *testing.T) { - resetEnv := func(t *testing.T) { - t.Helper() - for _, key := range envNames { - require.NoError(t, os.Setenv(key, "")) - } - } - - t.Run("merging priority", func(t *testing.T) { - resetEnv(t) - - require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) - require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) - require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) - require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) - require.NoError(t, os.Setenv(envGoghLogDate, "1")) - require.NoError(t, os.Setenv(envGoghLogTime, "1")) - require.NoError(t, os.Setenv(envGoghLogMicroSeconds, "1")) - require.NoError(t, os.Setenv(envGoghLogLongFile, "1")) - require.NoError(t, os.Setenv(envGoghLogShortFile, "1")) - require.NoError(t, os.Setenv(envGoghLogUTC, "1")) - require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) - - cfg1, err := GetEnvarConfig() - require.NoError(t, err) - - t.Run("full overwritten config", func(t *testing.T) { - require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx2")) - require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx2")) - require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh87")) - require.NoError(t, os.Setenv(envGoghLogLevel, "debug")) - require.NoError(t, os.Setenv(envGoghLogDate, "0")) - require.NoError(t, os.Setenv(envGoghLogTime, "0")) - require.NoError(t, os.Setenv(envGoghLogMicroSeconds, "0")) - require.NoError(t, os.Setenv(envGoghLogLongFile, "0")) - require.NoError(t, os.Setenv(envGoghLogShortFile, "0")) - require.NoError(t, os.Setenv(envGoghLogUTC, "0")) - require.NoError(t, os.Setenv(envGoghRoot, "/baz:/bux")) - - cfg2, err := GetEnvarConfig() - require.NoError(t, err) - - cfg := MergeConfig(cfg1, cfg2) // prior after config - assert.Equal(t, "tokenx2", cfg.GitHubToken()) - assert.Equal(t, "hostx2", cfg.GitHubHost()) - assert.Equal(t, "kyoh87", cfg.GitHubUser()) - assert.Equal(t, "debug", cfg.LogLevel()) - assert.Equal(t, 0, cfg.LogFlags()) - assert.Equal(t, []string{"/baz", "/bux"}, cfg.Root()) - assert.Equal(t, "/baz", cfg.PrimaryRoot()) - assert.Equal(t, os.Stderr, cfg.Stderr()) - assert.Equal(t, os.Stdout, cfg.Stdout()) - }) - - t.Run("no overwritten config", func(t *testing.T) { - resetEnv(t) - - cfg2, err := GetEnvarConfig() - require.NoError(t, err) - - cfg := MergeConfig(cfg1, cfg2) // prior after config - assert.Equal(t, "tokenx1", cfg.GitHubToken()) - assert.Equal(t, "hostx1", cfg.GitHubHost()) - assert.Equal(t, "kyoh86", cfg.GitHubUser()) - assert.Equal(t, "trace", cfg.LogLevel()) - assert.Equal(t, log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile|log.Lshortfile|log.LUTC, cfg.LogFlags()) - assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) - assert.Equal(t, "/foo", cfg.PrimaryRoot()) - assert.Equal(t, os.Stderr, cfg.Stderr()) - assert.Equal(t, os.Stdout, cfg.Stdout()) - }) - - }) - - t.Run("get context from envar", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) - require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) - require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) - require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) - require.NoError(t, os.Setenv(envGoghLogDate, "1")) - require.NoError(t, os.Setenv(envGoghLogTime, "1")) - require.NoError(t, os.Setenv(envGoghLogMicroSeconds, "1")) - require.NoError(t, os.Setenv(envGoghLogLongFile, "1")) - require.NoError(t, os.Setenv(envGoghLogShortFile, "1")) - require.NoError(t, os.Setenv(envGoghLogUTC, "1")) - require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) - cfg, err := GetEnvarConfig() - require.NoError(t, err) - assert.Equal(t, "tokenx1", cfg.GitHubToken()) - assert.Equal(t, "hostx1", cfg.GitHubHost()) - assert.Equal(t, "kyoh86", cfg.GitHubUser()) - assert.Equal(t, "trace", cfg.LogLevel()) - assert.Equal(t, log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile|log.Lshortfile|log.LUTC, cfg.LogFlags()) - assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) - assert.Equal(t, "/foo", cfg.PrimaryRoot()) - assert.Equal(t, os.Stderr, cfg.Stderr()) - assert.Equal(t, os.Stdout, cfg.Stdout()) - }) - - t.Run("get context from config", func(t *testing.T) { - resetEnv(t) - cfg, err := LoadConfig(bytes.NewBufferString(` -root: -- /foo -- /bar - -log: - level: trace - date: true - time: true - microseconds: true - longfile: true - shortfile: true - utc: true - -github: - token: tokenx1 - user: kyoh86 - host: hostx1 -`)) - require.NoError(t, err) - assert.Equal(t, "tokenx1", cfg.GitHubToken()) - assert.Equal(t, "hostx1", cfg.GitHubHost()) - assert.Equal(t, "kyoh86", cfg.GitHubUser()) - assert.Equal(t, "trace", cfg.LogLevel()) - assert.Equal(t, log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile|log.Lshortfile|log.LUTC, cfg.LogFlags()) - assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) - assert.Equal(t, "/foo", cfg.PrimaryRoot()) - assert.Equal(t, os.Stderr, cfg.Stderr()) - assert.Equal(t, os.Stdout, cfg.Stdout()) - }) - - t.Run("get default context", func(t *testing.T) { - resetEnv(t) - cfg := DefaultConfig() - assert.Equal(t, "", cfg.GitHubToken()) - assert.Equal(t, "github.com", cfg.GitHubHost()) - assert.Equal(t, "", cfg.GitHubUser()) - assert.Equal(t, "warn", cfg.LogLevel()) - assert.NotEmpty(t, cfg.Root()) - assert.NotEmpty(t, cfg.PrimaryRoot()) - assert.Equal(t, os.Stderr, cfg.Stderr()) - assert.Equal(t, os.Stdout, cfg.Stdout()) - }) - - t.Run("expects roots are not duplicated", func(t *testing.T) { - resetEnv(t) - require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar:/bar:/foo")) - cfg, err := GetEnvarConfig() - require.NoError(t, err) - assert.NoError(t, err) - assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) - }) -} diff --git a/config/get.go b/config/get.go index 4aeae3ed..6ce95099 100644 --- a/config/get.go +++ b/config/get.go @@ -47,7 +47,7 @@ const ( var defaultConfig = Config{ Log: LogConfig{ Level: DefaultLogLevel, - Time: TrueConfig, + Time: TrueOption, }, GitHub: GitHubConfig{ Host: DefaultHost, @@ -77,6 +77,10 @@ func LoadConfig(r io.Reader) (config *Config, err error) { return } +func SaveConfig(w io.Writer, config *Config) error { + return yaml.NewEncoder(w).Encode(config) +} + func GetEnvarConfig() (config *Config, err error) { config = &Config{} err = envdecode.Decode(config) @@ -86,46 +90,3 @@ func GetEnvarConfig() (config *Config, err error) { config.VRoot = util.UniqueStringArray(config.VRoot) return } - -func MergeConfig(base *Config, override ...*Config) *Config { - c := *base - for _, o := range override { - c.Log.Level = mergeStringOption(c.Log.Level, o.Log.Level) - c.Log.Date = mergeBoolOption(c.Log.Date, o.Log.Date) - c.Log.Time = mergeBoolOption(c.Log.Time, o.Log.Time) - c.Log.MicroSeconds = mergeBoolOption(c.Log.MicroSeconds, o.Log.MicroSeconds) - c.Log.LongFile = mergeBoolOption(c.Log.LongFile, o.Log.LongFile) - c.Log.ShortFile = mergeBoolOption(c.Log.ShortFile, o.Log.ShortFile) - c.Log.UTC = mergeBoolOption(c.Log.UTC, o.Log.UTC) - c.VRoot = mergeStringArrayOption(c.VRoot, o.VRoot) - c.GitHub.Token = mergeStringOption(c.GitHub.Token, o.GitHub.Token) - c.GitHub.User = mergeStringOption(c.GitHub.User, o.GitHub.User) - c.GitHub.Host = mergeStringOption(c.GitHub.Host, o.GitHub.Host) - } - return &c -} - -func mergeBoolOption(base, override BoolConfig) BoolConfig { - switch { - case override.filled: - return override - case base.filled: - return base - default: - return BoolConfig{} - } -} - -func mergeStringOption(base, override string) string { - if override != "" { - return override - } - return base -} - -func mergeStringArrayOption(base, override []string) []string { - if len(override) > 0 { - return override - } - return base -} diff --git a/config/get_test.go b/config/get_test.go new file mode 100644 index 00000000..1b44c7e6 --- /dev/null +++ b/config/get_test.go @@ -0,0 +1,136 @@ +package config + +import ( + "bytes" + "log" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func resetEnv(t *testing.T) { + t.Helper() + for _, key := range envNames { + require.NoError(t, os.Setenv(key, "")) + } +} + +func TestDefaultConfig(t *testing.T) { + resetEnv(t) + cfg := DefaultConfig() + assert.Equal(t, "", cfg.GitHubToken()) + assert.Equal(t, "github.com", cfg.GitHubHost()) + assert.Equal(t, "", cfg.GitHubUser()) + assert.Equal(t, "warn", cfg.LogLevel()) + assert.NotEmpty(t, cfg.Root()) + assert.NotEmpty(t, cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) +} + +func TestLoadConfig(t *testing.T) { + t.Run("success", func(t *testing.T) { + resetEnv(t) + cfg, err := LoadConfig(bytes.NewBufferString(` +root: +- /foo +- /bar + +log: + level: trace + date: "yes" + time: "yes" + microseconds: yes + longfile: "yes" + shortfile: "yes" + utc: "yes" + +github: + token: tokenx1 + user: kyoh86 + host: hostx1 +`)) + require.NoError(t, err) + assert.Equal(t, "", cfg.GitHubToken(), "token should not be saved in file") + assert.Equal(t, "hostx1", cfg.GitHubHost()) + assert.Equal(t, "kyoh86", cfg.GitHubUser()) + assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile|log.Lshortfile|log.LUTC, cfg.LogFlags()) + assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) + assert.Equal(t, "/foo", cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) + }) + t.Run("invalid format", func(t *testing.T) { + resetEnv(t) + _, err := LoadConfig(bytes.NewBufferString(`{`)) + assert.Error(t, err) + }) +} + +func TestSaveConfig(t *testing.T) { + t.Run("success", func(t *testing.T) { + resetEnv(t) + + var buf bytes.Buffer + var cfg Config + cfg.GitHub.Token = "token1" + cfg.GitHub.Host = "hostx1" + cfg.GitHub.User = "kyoh86" + cfg.Log.Level = "trace" + cfg.Log.Date = TrueOption + cfg.Log.Time = FalseOption + cfg.Log.MicroSeconds = TrueOption + cfg.Log.LongFile = TrueOption + cfg.Log.ShortFile = TrueOption + cfg.Log.UTC = TrueOption + cfg.VRoot = []string{"/foo", "/bar"} + + require.NoError(t, SaveConfig(&buf, &cfg)) + + output := buf.String() + assert.Contains(t, output, "root:") + assert.Contains(t, output, "- /foo") + assert.Contains(t, output, "- /bar") + assert.Contains(t, output, "log:") + assert.Contains(t, output, " level: trace") + assert.Contains(t, output, ` date: "yes"`) + assert.Contains(t, output, ` time: "no"`) + assert.Contains(t, output, ` microseconds: "yes"`) + assert.Contains(t, output, ` longfile: "yes"`) + assert.Contains(t, output, ` shortfile: "yes"`) + assert.Contains(t, output, ` utc: "yes"`) + assert.Contains(t, output, "github:") + assert.NotContains(t, output, "tokenx1") + assert.Contains(t, output, " user: kyoh86") + assert.Contains(t, output, " host: hostx1") + }) +} + +func TestGetEnvarConfig(t *testing.T) { + resetEnv(t) + require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) + require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) + require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) + require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) + require.NoError(t, os.Setenv(envGoghLogDate, "yes")) + require.NoError(t, os.Setenv(envGoghLogTime, "yes")) + require.NoError(t, os.Setenv(envGoghLogMicroSeconds, "yes")) + require.NoError(t, os.Setenv(envGoghLogLongFile, "yes")) + require.NoError(t, os.Setenv(envGoghLogShortFile, "yes")) + require.NoError(t, os.Setenv(envGoghLogUTC, "yes")) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar:/bar:/foo")) + cfg, err := GetEnvarConfig() + require.NoError(t, err) + assert.Equal(t, "tokenx1", cfg.GitHubToken()) + assert.Equal(t, "hostx1", cfg.GitHubHost()) + assert.Equal(t, "kyoh86", cfg.GitHubUser()) + assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile|log.Lshortfile|log.LUTC, cfg.LogFlags()) + assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root(), "expects roots are not duplicated") + assert.Equal(t, "/foo", cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) +} diff --git a/config/merge.go b/config/merge.go new file mode 100644 index 00000000..7d865ad0 --- /dev/null +++ b/config/merge.go @@ -0,0 +1,44 @@ +package config + +func MergeConfig(base *Config, override ...*Config) *Config { + c := *base + for _, o := range override { + c.Log.Level = mergeStringOption(c.Log.Level, o.Log.Level) + c.Log.Date = mergeBoolOption(c.Log.Date, o.Log.Date) + c.Log.Time = mergeBoolOption(c.Log.Time, o.Log.Time) + c.Log.MicroSeconds = mergeBoolOption(c.Log.MicroSeconds, o.Log.MicroSeconds) + c.Log.LongFile = mergeBoolOption(c.Log.LongFile, o.Log.LongFile) + c.Log.ShortFile = mergeBoolOption(c.Log.ShortFile, o.Log.ShortFile) + c.Log.UTC = mergeBoolOption(c.Log.UTC, o.Log.UTC) + c.VRoot = mergePathListOption(c.VRoot, o.VRoot) + c.GitHub.Token = mergeStringOption(c.GitHub.Token, o.GitHub.Token) + c.GitHub.User = mergeStringOption(c.GitHub.User, o.GitHub.User) + c.GitHub.Host = mergeStringOption(c.GitHub.Host, o.GitHub.Host) + } + return &c +} + +func mergeBoolOption(base, override BoolOption) BoolOption { + switch { + case override != EmptyBoolOption: + return override + case base != EmptyBoolOption: + return base + default: + return EmptyBoolOption + } +} + +func mergeStringOption(base, override string) string { + if override != "" { + return override + } + return base +} + +func mergePathListOption(base, override []string) []string { + if len(override) > 0 { + return override + } + return base +} diff --git a/config/merge_test.go b/config/merge_test.go new file mode 100644 index 00000000..e8379816 --- /dev/null +++ b/config/merge_test.go @@ -0,0 +1,83 @@ +package config + +import ( + "log" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMergeConfig(t *testing.T) { + resetEnv(t) + + require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx1")) + require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx1")) + require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh86")) + require.NoError(t, os.Setenv(envGoghLogLevel, "trace")) + require.NoError(t, os.Setenv(envGoghLogDate, "yes")) + require.NoError(t, os.Setenv(envGoghLogTime, "yes")) + require.NoError(t, os.Setenv(envGoghLogMicroSeconds, "yes")) + require.NoError(t, os.Setenv(envGoghLogLongFile, "yes")) + require.NoError(t, os.Setenv(envGoghLogShortFile, "yes")) + require.NoError(t, os.Setenv(envGoghLogUTC, "yes")) + require.NoError(t, os.Setenv(envGoghRoot, "/foo:/bar")) + + cfg1, err := GetEnvarConfig() + require.NoError(t, err) + + t.Run("full overwritten config", func(t *testing.T) { + require.NoError(t, os.Setenv(envGoghGitHubToken, "tokenx2")) + require.NoError(t, os.Setenv(envGoghGitHubHost, "hostx2")) + require.NoError(t, os.Setenv(envGoghGitHubUser, "kyoh87")) + require.NoError(t, os.Setenv(envGoghLogLevel, "debug")) + require.NoError(t, os.Setenv(envGoghLogDate, "no")) + require.NoError(t, os.Setenv(envGoghLogTime, "no")) + require.NoError(t, os.Setenv(envGoghLogMicroSeconds, "no")) + require.NoError(t, os.Setenv(envGoghLogLongFile, "no")) + require.NoError(t, os.Setenv(envGoghLogShortFile, "no")) + require.NoError(t, os.Setenv(envGoghLogUTC, "no")) + require.NoError(t, os.Setenv(envGoghRoot, "/baz:/bux")) + + cfg2, err := GetEnvarConfig() + require.NoError(t, err) + + cfg := MergeConfig(cfg1, cfg2) // prior after config + assert.Equal(t, "tokenx2", cfg.GitHubToken()) + assert.Equal(t, "hostx2", cfg.GitHubHost()) + assert.Equal(t, "kyoh87", cfg.GitHubUser()) + assert.Equal(t, "debug", cfg.LogLevel()) + assert.Equal(t, 0, cfg.LogFlags()) + assert.Equal(t, []string{"/baz", "/bux"}, cfg.Root()) + assert.Equal(t, "/baz", cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) + }) + + t.Run("no overwritten config", func(t *testing.T) { + resetEnv(t) + + cfg2, err := GetEnvarConfig() + require.NoError(t, err) + + cfg := MergeConfig(cfg1, cfg2) // prior after config + assert.Equal(t, "tokenx1", cfg.GitHubToken()) + assert.Equal(t, "hostx1", cfg.GitHubHost()) + assert.Equal(t, "kyoh86", cfg.GitHubUser()) + assert.Equal(t, "trace", cfg.LogLevel()) + assert.Equal(t, log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile|log.Lshortfile|log.LUTC, cfg.LogFlags()) + assert.Equal(t, []string{"/foo", "/bar"}, cfg.Root()) + assert.Equal(t, "/foo", cfg.PrimaryRoot()) + assert.Equal(t, os.Stderr, cfg.Stderr()) + assert.Equal(t, os.Stdout, cfg.Stdout()) + }) + + resetEnv(t) + assert.Equal(t, EmptyBoolOption, mergeBoolOption(EmptyBoolOption, EmptyBoolOption)) + assert.Equal(t, TrueOption, mergeBoolOption(TrueOption, EmptyBoolOption)) + assert.Equal(t, FalseOption, mergeBoolOption(FalseOption, EmptyBoolOption)) + assert.Equal(t, TrueOption, mergeBoolOption(EmptyBoolOption, TrueOption)) + assert.Equal(t, FalseOption, mergeBoolOption(TrueOption, FalseOption)) + assert.Equal(t, TrueOption, mergeBoolOption(FalseOption, TrueOption)) +} diff --git a/config/string_array.go b/config/path_list.go similarity index 54% rename from config/string_array.go rename to config/path_list.go index 220ca1f3..b62914d6 100644 --- a/config/string_array.go +++ b/config/path_list.go @@ -1,22 +1,24 @@ package config -import "strings" +import ( + "path/filepath" +) -type StringArrayConfig []string +type PathListOption []string // Decode implements the interface `envdecode.Decoder` -func (c *StringArrayConfig) Decode(repl string) error { - *c = strings.Split(repl, ":") +func (c *PathListOption) Decode(repl string) error { + *c = filepath.SplitList(repl) return nil } // MarshalYAML implements the interface `yaml.Marshaler` -func (c *StringArrayConfig) MarshalYAML() (interface{}, error) { +func (c *PathListOption) MarshalYAML() (interface{}, error) { return []string(*c), nil } // UnmarshalYAML implements the interface `yaml.Unmarshaler` -func (c *StringArrayConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (c *PathListOption) UnmarshalYAML(unmarshal func(interface{}) error) error { var parsed []string if err := unmarshal(&parsed); err != nil { return err diff --git a/config/path_list_test.go b/config/path_list_test.go new file mode 100644 index 00000000..23d0de38 --- /dev/null +++ b/config/path_list_test.go @@ -0,0 +1,54 @@ +package config + +import ( + "bytes" + "os" + "strings" + "testing" + + "github.com/joeshaw/envdecode" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + yaml "gopkg.in/yaml.v2" +) + +func TestPathListOption(t *testing.T) { + + type testStruct struct { + PathList PathListOption `env:"PATH_LIST" yaml:"paths,omitempty"` + } + + t.Run("encode to yaml", func(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, yaml.NewEncoder(&buf).Encode(testStruct{})) + assert.Equal(t, "{}", strings.TrimSpace(buf.String())) + + buf.Reset() + require.NoError(t, yaml.NewEncoder(&buf).Encode(testStruct{PathList: PathListOption{}})) + assert.Equal(t, "{}", strings.TrimSpace(buf.String())) + + buf.Reset() + require.NoError(t, yaml.NewEncoder(&buf).Encode(testStruct{PathList: PathListOption{"/foo", "/bar"}})) + assert.Equal(t, "paths:\n- /foo\n- /bar", strings.TrimSpace(buf.String())) + }) + t.Run("decode from YAML", func(t *testing.T) { + var testValue testStruct + require.NoError(t, yaml.Unmarshal([]byte(`{}`), &testValue)) + assert.Equal(t, PathListOption(nil), testValue.PathList) + + require.NoError(t, yaml.Unmarshal([]byte(`paths: []`), &testValue)) + assert.Equal(t, PathListOption{}, testValue.PathList) + + require.NoError(t, yaml.Unmarshal([]byte(`paths: ["/foo", "/bar"]`), &testValue)) + assert.Equal(t, PathListOption{"/foo", "/bar"}, testValue.PathList) + + assert.Error(t, yaml.Unmarshal([]byte(`paths: invalid`), &testValue)) + }) + t.Run("get from envar", func(t *testing.T) { + var testValue testStruct + resetEnv(t) + require.NoError(t, os.Setenv("PATH_LIST", "/foo:/bar")) + require.NoError(t, envdecode.Decode(&testValue)) + assert.Equal(t, PathListOption{"/foo", "/bar"}, testValue.PathList) + }) +} diff --git a/gogh/context.go b/gogh/context.go index 3aa37c54..f8e93de3 100644 --- a/gogh/context.go +++ b/gogh/context.go @@ -15,6 +15,11 @@ type Context interface { GitHubHost() string LogLevel() string LogFlags() int // log.Lxxx flags + LogDate() bool + LogTime() bool + LogLongFile() bool + LogShortFile() bool + LogUTC() bool Root() []string PrimaryRoot() string } diff --git a/gogh/context_test.go b/gogh/context_test.go index 82ea6d88..e330bc54 100644 --- a/gogh/context_test.go +++ b/gogh/context_test.go @@ -7,13 +7,19 @@ import ( type implContext struct { context.Context - stdout io.Writer - stderr io.Writer - gitHubUser string - gitHubToken string - gitHubHost string - logLevel string - root []string + stdout io.Writer + stderr io.Writer + gitHubUser string + gitHubToken string + gitHubHost string + logLevel string + logFlags int + logDate bool + logTime bool + logLongFile bool + logShortFile bool + logUTC bool + root []string } func (c *implContext) Stdout() io.Writer { @@ -40,6 +46,16 @@ func (c *implContext) LogLevel() string { return c.logLevel } +func (c *implContext) LogFlags() int { + return c.logFlags +} + +func (c *implContext) LogDate() bool { return c.logDate } +func (c *implContext) LogTime() bool { return c.logTime } +func (c *implContext) LogLongFile() bool { return c.logLongFile } +func (c *implContext) LogShortFile() bool { return c.logShortFile } +func (c *implContext) LogUTC() bool { return c.logUTC } + func (c *implContext) Root() []string { return c.root } diff --git a/gogh/formatter_test.go b/gogh/formatter_test.go index f0355ba6..e1ec5e3c 100644 --- a/gogh/formatter_test.go +++ b/gogh/formatter_test.go @@ -55,9 +55,6 @@ func TestFormatter(t *testing.T) { formatter.Add(project1) formatter.Add(project2) assert.Equal(t, 2, formatter.Len()) - var buf bytes.Buffer - require.NoError(t, formatter.PrintAll(&buf, ":")) - assert.Equal(t, `/go/src/github.com/kyoh86/foo:/go/src/github.com/kyoh86/bar:`, buf.String()) }) t.Run("writer error by full path formatter", func(t *testing.T) { project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") diff --git a/gogh/validation_test.go b/gogh/validation_test.go index d722b046..d75eb8fb 100644 --- a/gogh/validation_test.go +++ b/gogh/validation_test.go @@ -31,6 +31,7 @@ func TestValidateRoots(t *testing.T) { require.NoError(t, err) assert.EqualError(t, ValidateRoots([]string{}), "no root", "fail when no path in root") assert.NoError(t, ValidateRoots([]string{"/path/to/not/existing", tmp})) + assert.Error(t, ValidateRoots([]string{"\x00", tmp})) } func TestValidateContext(t *testing.T) { diff --git a/internal/mainutil/mainutil.go b/internal/mainutil/mainutil.go index c8b2854d..0519b00b 100644 --- a/internal/mainutil/mainutil.go +++ b/internal/mainutil/mainutil.go @@ -13,7 +13,7 @@ import ( func setConfigFlag(cmd *kingpin.CmdClause, configFile *string) { cmd.Flag("config", "configuration file"). - Default(filepath.Join(xdg.CacheHome(), "gogh", "config.toml")). + Default(filepath.Join(xdg.ConfigHome(), "gogh", "config.yaml")). Envar("GOGH_CONFIG"). StringVar(configFile) } @@ -32,17 +32,17 @@ func initLog(ctx gogh.Context) error { } func currentConfig(configFile string) (*config.Config, *config.Config, error) { - var fileConfig *config.Config + var fileCfg *config.Config file, err := os.Open(configFile) switch { case err == nil: defer file.Close() - fileConfig, err = config.LoadConfig(file) + fileCfg, err = config.LoadConfig(file) if err != nil { return nil, nil, err } case os.IsNotExist(err): - fileConfig = &config.Config{} + fileCfg = &config.Config{} default: return nil, nil, err } @@ -50,26 +50,26 @@ func currentConfig(configFile string) (*config.Config, *config.Config, error) { if err != nil { return nil, nil, err } - config := config.MergeConfig(config.DefaultConfig(), fileConfig, envarConfig) - if err := gogh.ValidateContext(config); err != nil { + cfg := config.MergeConfig(config.DefaultConfig(), fileCfg, envarConfig) + if err := gogh.ValidateContext(cfg); err != nil { return nil, nil, err } - return fileConfig, config, nil + return fileCfg, cfg, nil } func WrapCommand(cmd *kingpin.CmdClause, f func(gogh.Context) error) (string, func() error) { var configFile string setConfigFlag(cmd, &configFile) return cmd.FullCommand(), func() error { - _, config, err := currentConfig(configFile) + _, cfg, err := currentConfig(configFile) if err != nil { return err } - if err := initLog(config); err != nil { + if err := initLog(cfg); err != nil { return err } - return f(config) + return f(cfg) } } @@ -77,19 +77,27 @@ func WrapConfigurableCommand(cmd *kingpin.CmdClause, f func(*config.Config) erro var configFile string setConfigFlag(cmd, &configFile) return cmd.FullCommand(), func() error { - fileConfig, config, err := currentConfig(configFile) + fileCfg, cfg, err := currentConfig(configFile) if err != nil { return err } - if err := initLog(config); err != nil { + if err := initLog(cfg); err != nil { return err } - if err = f(fileConfig); err != nil { + if err = f(fileCfg); err != nil { return err } - //TODO: save fileConfig - return nil + + if err := os.MkdirAll(filepath.Dir(configFile), 0744); err != nil { + return err + } + file, err := os.OpenFile(configFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer file.Close() + return config.SaveConfig(file, fileCfg) } } diff --git a/main.go b/main.go index 8abee584..2184d132 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,7 @@ func main() { for _, f := range []func(*kingpin.Application) (string, func() error){ configGetAll, configGet, - configSet, + configPut, configUnset, get, @@ -55,8 +55,8 @@ func main() { func configGetAll(app *kingpin.Application) (string, func() error) { cmd := app.GetCommand("config").Command("get-all", "get all options") - return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { - return command.ConfigGetAll(ctx) + return mainutil.WrapConfigurableCommand(cmd, func(cfg *config.Config) error { + return command.ConfigGetAll(cfg) }) } @@ -67,22 +67,22 @@ func configGet(app *kingpin.Application) (string, func() error) { cmd := app.GetCommand("config").Command("get", "get an option") cmd.Arg("name", "option name").Required().StringVar(&name) - return mainutil.WrapCommand(cmd, func(ctx gogh.Context) error { - return command.ConfigGet(ctx, name) + return mainutil.WrapConfigurableCommand(cmd, func(cfg *config.Config) error { + return command.ConfigGet(cfg, name) }) } -func configSet(app *kingpin.Application) (string, func() error) { +func configPut(app *kingpin.Application) (string, func() error) { var ( name string value string ) - cmd := app.GetCommand("config").Command("set", "set an option") + cmd := app.GetCommand("config").Command("put", "put an option") cmd.Arg("name", "option name").Required().StringVar(&name) cmd.Arg("value", "option value").Required().StringVar(&value) - return mainutil.WrapConfigurableCommand(cmd, func(config *config.Config) error { - return command.ConfigSet(config, name, value) + return mainutil.WrapConfigurableCommand(cmd, func(cfg *config.Config) error { + return command.ConfigPut(cfg, name, value) }) } @@ -93,8 +93,8 @@ func configUnset(app *kingpin.Application) (string, func() error) { cmd := app.GetCommand("config").Command("unset", "unset an option") cmd.Arg("name", "option name").Required().StringVar(&name) - return mainutil.WrapConfigurableCommand(cmd, func(config *config.Config) error { - return command.ConfigUnset(config, name) + return mainutil.WrapConfigurableCommand(cmd, func(cfg *config.Config) error { + return command.ConfigUnset(cfg, name) }) } From 742f5767a26777cabcf59c4d46318fc197af5c32 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Thu, 28 Mar 2019 15:21:15 +0900 Subject: [PATCH 13/30] MarshalYAML should be implemented for instance --- config/path_list.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/path_list.go b/config/path_list.go index b62914d6..435d705c 100644 --- a/config/path_list.go +++ b/config/path_list.go @@ -13,8 +13,8 @@ func (c *PathListOption) Decode(repl string) error { } // MarshalYAML implements the interface `yaml.Marshaler` -func (c *PathListOption) MarshalYAML() (interface{}, error) { - return []string(*c), nil +func (c PathListOption) MarshalYAML() (interface{}, error) { + return []string(c), nil } // UnmarshalYAML implements the interface `yaml.Unmarshaler` From ae93af6ae5f8c24d913002b957c1124cd39de885 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Thu, 28 Mar 2019 15:36:51 +0900 Subject: [PATCH 14/30] Cover more --- config/bool_test.go | 1 + gogh/local_test.go | 5 +++++ gogh/validation_test.go | 40 ++++++++++++++++++++++++++++++++-------- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/config/bool_test.go b/config/bool_test.go index 288062d2..aad3ca96 100644 --- a/config/bool_test.go +++ b/config/bool_test.go @@ -48,6 +48,7 @@ func TestBoolOption(t *testing.T) { assert.Equal(t, EmptyBoolOption, testValue.Bool) assert.Error(t, yaml.Unmarshal([]byte(`bool: invalid`), &testValue)) + assert.Error(t, yaml.Unmarshal([]byte("bool:\n- invalid"), &testValue)) }) t.Run("get from envar", func(t *testing.T) { var testValue testStruct diff --git a/gogh/local_test.go b/gogh/local_test.go index 25ee92bd..b22fc808 100644 --- a/gogh/local_test.go +++ b/gogh/local_test.go @@ -93,6 +93,11 @@ func TestFindOrNewProject(t *testing.T) { _, err := NewProject(&ctx, parseURL(t, "ssh://git@example.com/kyoh86/gogh.git")) assert.EqualError(t, err, `not supported host: "example.com"`) }) + t.Run("fail with invalid root", func(t *testing.T) { + ctx := implContext{root: []string{"/\x00"}, gitHubUser: "kyoh86", gitHubHost: "github.com"} + _, err := FindOrNewProject(&ctx, parseURL(t, "ssh://git@github.com/kyoh86/gogh.git")) + assert.Error(t, err) + }) t.Run("existing repository", func(t *testing.T) { // Create same name repository require.NoError(t, os.MkdirAll(filepath.Join(tmp, "github.com", "kyoh85", "gogh", ".git"), 0755)) diff --git a/gogh/validation_test.go b/gogh/validation_test.go index d75eb8fb..621d04aa 100644 --- a/gogh/validation_test.go +++ b/gogh/validation_test.go @@ -35,12 +35,36 @@ func TestValidateRoots(t *testing.T) { } func TestValidateContext(t *testing.T) { - ctx := &implContext{ - root: []string{"/path/to/not/existing"}, - logLevel: "warn", - } - ctx.gitHubUser = "" - assert.Error(t, ValidateContext(ctx), "fail when empty owner is given") - ctx.gitHubUser = "kyoh86" - assert.NoError(t, ValidateContext(ctx), "success") + t.Run("invalid root", func(t *testing.T) { + ctx := &implContext{ + root: []string{"/\x00"}, + logLevel: "warn", + gitHubUser: "kyoh86", + } + assert.Error(t, ValidateContext(ctx)) + }) + t.Run("invalid owner", func(t *testing.T) { + ctx := &implContext{ + root: []string{"/path/to/not/existing"}, + logLevel: "warn", + gitHubUser: "", + } + assert.Error(t, ValidateContext(ctx)) + }) + t.Run("invalid loglevel", func(t *testing.T) { + ctx := &implContext{ + root: []string{"/path/to/not/existing"}, + logLevel: "invalid", + gitHubUser: "kyoh86", + } + assert.Error(t, ValidateContext(ctx)) + }) + t.Run("valid context", func(t *testing.T) { + ctx := &implContext{ + root: []string{"/path/to/not/existing"}, + logLevel: "warn", + gitHubUser: "kyoh86", + } + assert.NoError(t, ValidateContext(ctx)) + }) } From 0aa20cfe0572ce095da6ce7ea9f2fecfaf4c1148 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Thu, 28 Mar 2019 15:43:31 +0900 Subject: [PATCH 15/30] Cover more --- gogh/local_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gogh/local_test.go b/gogh/local_test.go index b22fc808..68987866 100644 --- a/gogh/local_test.go +++ b/gogh/local_test.go @@ -38,6 +38,12 @@ func TestParseProject(t *testing.T) { assert.False(t, p.IsInPrimaryRoot(&ctx)) }) + t.Run("expect to fail to parse relative path", func(t *testing.T) { + r, err := parseProject(&ctx, tmp, "./github.com/kyoh86/gogh/gogh") + assert.NotNil(t, err) + assert.Nil(t, r) + }) + t.Run("expect to fail to parse unsupported depth", func(t *testing.T) { r, err := parseProject(&ctx, tmp, filepath.Join(tmp, "github.com/kyoh86/gogh/gogh")) assert.NotNil(t, err) From f6d27529ac0a88c26453dfcc63edeedbdfd64fce Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Thu, 28 Mar 2019 15:49:02 +0900 Subject: [PATCH 16/30] count lines --- command/config_test.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 command/config_test.go diff --git a/command/config_test.go b/command/config_test.go new file mode 100644 index 00000000..d47dcf0d --- /dev/null +++ b/command/config_test.go @@ -0,0 +1 @@ +package command From d184b1663c898c9ac2b5a9843cc3b9cd93c403db Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Thu, 28 Mar 2019 18:13:32 +0900 Subject: [PATCH 17/30] Refactor: create command/internal --- command/bulk.go | 3 +- command/fork.go | 2 +- command/get.go | 4 +- command/git.go | 68 ++++++-------------- command/hub.go | 101 ++++-------------------------- command/internal/git.go | 63 +++++++++++++++++++ command/internal/hub.go | 107 ++++++++++++++++++++++++++++++++ command/{ => internal}/param.go | 2 +- command/{ => internal}/run.go | 2 +- command/new.go | 4 +- config/config.go | 4 ++ go.mod | 4 ++ go.sum | 8 +++ gogh/context.go | 1 + gogh/context_test.go | 65 ------------------- gogh/formatter_test.go | 35 ++++++----- gogh/local_test.go | 25 ++++---- gogh/repo_test.go | 19 +++--- gogh/validation_test.go | 33 +++++----- internal/context/mock.go | 70 +++++++++++++++++++++ 20 files changed, 355 insertions(+), 265 deletions(-) create mode 100644 command/internal/git.go create mode 100644 command/internal/hub.go rename command/{ => internal}/param.go (94%) rename command/{ => internal}/run.go (97%) delete mode 100644 gogh/context_test.go create mode 100644 internal/context/mock.go diff --git a/command/bulk.go b/command/bulk.go index 9df1f05c..fe0297ab 100644 --- a/command/bulk.go +++ b/command/bulk.go @@ -3,7 +3,6 @@ package command import ( "bufio" "io" - "os" "os/exec" "github.com/kyoh86/gogh/gogh" @@ -33,7 +32,7 @@ func Pipe(ctx gogh.Context, update, withSSH, shallow bool, command string, comma // Bulk get repositories specified in stdin. func Bulk(ctx gogh.Context, update, withSSH, shallow bool) error { - return bulkFromReader(ctx, os.Stdin, update, withSSH, shallow) + return bulkFromReader(ctx, ctx.Stdin(), update, withSSH, shallow) } // bulkFromReader bulk get repositories specified in reader. diff --git a/command/fork.go b/command/fork.go index 81ab90a1..02d91647 100644 --- a/command/fork.go +++ b/command/fork.go @@ -15,7 +15,7 @@ func Fork(ctx gogh.Context, update, withSSH, shallow, noRemote bool, remoteName return err } log.Printf("info: Forking a repository") - if err := hubFork(ctx, project, repo, noRemote, remoteName, organization); err != nil { + if err := hub().Fork(ctx, project, repo, noRemote, remoteName, organization); err != nil { return err } diff --git a/command/get.go b/command/get.go index 13965ad5..4acbdfc4 100644 --- a/command/get.go +++ b/command/get.go @@ -29,11 +29,11 @@ func Get(ctx gogh.Context, update, withSSH, shallow bool, repo *gogh.Repo) error } if !project.Exists { log.Println("info: Clone", fmt.Sprintf("%s -> %s", repoURL, project.FullPath)) - return gitClone(ctx, repoURL, project.FullPath, shallow) + return git().Clone(ctx, project, repoURL, shallow) } if update { log.Println("info: Update", project.FullPath) - return gitUpdate(ctx, project.FullPath) + return git().Update(ctx, project) } log.Println("warn: Exists", project.FullPath) return nil diff --git a/command/git.go b/command/git.go index 4d09053d..406d2dce 100644 --- a/command/git.go +++ b/command/git.go @@ -2,62 +2,32 @@ package command import ( "net/url" - "os" - "os/exec" - "path/filepath" + "github.com/kyoh86/gogh/command/internal" "github.com/kyoh86/gogh/gogh" ) -func gitInit( - ctx gogh.Context, - bare bool, - template string, - separateGitDir string, - shared gogh.ProjectShared, - directory string, -) error { - args := []string{"init"} - args = appendIf(args, "--bare", bare) - args = appendIfFilled(args, "--template", template) - args = appendIfFilled(args, "--separate-git-dir", separateGitDir) - args = appendIfFilled(args, "--shared", shared.String()) - args = append(args, directory) - cmd := exec.Command("git", args...) - cmd.Stdin = os.Stdin - cmd.Stdout = ctx.Stdout() - cmd.Stderr = ctx.Stderr() - return execCommand(cmd) +type gitClient interface { + Init(ctx gogh.Context, project *gogh.Project, bare bool, template, separateGitDir string, shared gogh.ProjectShared) error + Clone(ctx gogh.Context, project *gogh.Project, remote *url.URL, shallow bool) error + Update(ctx gogh.Context, project *gogh.Project) error } -// gitClone git repository -func gitClone(ctx gogh.Context, remote *url.URL, local string, shallow bool) error { - dir, _ := filepath.Split(local) - err := os.MkdirAll(dir, 0755) - if err != nil { - return err - } - - args := []string{"clone"} - if shallow { - args = append(args, "--depth", "1") - } - args = append(args, remote.String(), local) - - cmd := exec.Command("git", args...) - cmd.Stdin = os.Stdin - cmd.Stdout = ctx.Stdout() - cmd.Stderr = ctx.Stderr() - return execCommand(cmd) +type mockGitClient struct { } -// gitUpdate pulls changes from remote repository -func gitUpdate(ctx gogh.Context, local string) error { - cmd := exec.Command("git", "pull", "--ff-only") - cmd.Stdin = os.Stdin - cmd.Stdout = ctx.Stdout() - cmd.Stderr = ctx.Stderr() - cmd.Dir = local +func (i *mockGitClient) Init(ctx gogh.Context, project *gogh.Project, bare bool, template, separateGitDir string, shared gogh.ProjectShared) error { + return nil +} + +func (i *mockGitClient) Clone(ctx gogh.Context, project *gogh.Project, remote *url.URL, shallow bool) error { + return nil +} + +func (i *mockGitClient) Update(ctx gogh.Context, project *gogh.Project) error { return nil } + +var defaultGitClient gitClient = &internal.GitClient{} - return execCommand(cmd) +func git() gitClient { + return defaultGitClient } diff --git a/command/hub.go b/command/hub.go index 0dd7f6fe..3c24cc2c 100644 --- a/command/hub.go +++ b/command/hub.go @@ -1,105 +1,30 @@ package command import ( - "log" "net/url" - "os" - "strings" - "github.com/github/hub/commands" + "github.com/kyoh86/gogh/command/internal" "github.com/kyoh86/gogh/gogh" ) -func chdirTmp(dir string) (func() error, error) { - log.Printf("debug: Changing working directory to %s", dir) - cd, err := os.Getwd() - if err != nil { - return nil, err - } - if err := os.Chdir(dir); err != nil { - return nil, err - } - return func() error { - log.Printf("debug: Changing back working directory to %s", cd) - return os.Chdir(cd) - }, nil +type hubClient interface { + Fork(ctx gogh.Context, project *gogh.Project, repo *gogh.Repo, noRemote bool, remoteName, organization string) error + Create(ctx gogh.Context, project *gogh.Project, repo *gogh.Repo, description string, homepage *url.URL, private, browse, clipboard bool) error } -func hubFork( - ctx gogh.Context, - project *gogh.Project, - repo *gogh.Repo, - noRemote bool, - remoteName string, - organization string, -) (retErr error) { - tear, err := chdirTmp(project.FullPath) - if err != nil { - return err - } - defer func() { - if err := tear(); err != nil && retErr == nil { - retErr = err - } - }() +type mockHubClient struct { +} - hubArgs := []string{"hub", "fork"} - hubArgs = appendIf(hubArgs, "--no-remote", noRemote) - hubArgs = appendIfFilled(hubArgs, "--remote-name", remoteName) - hubArgs = appendIfFilled(hubArgs, "--organization", organization) - // call hub fork - if err := os.Setenv("GITHUB_HOST", repo.Host(ctx)); err != nil { - return err - } - if err := os.Setenv("GITHUB_TOKEN", ctx.GitHubToken()); err != nil { - return err - } - log.Printf("debug: Calling `%s`", strings.Join(hubArgs, " ")) - if err := commands.CmdRunner.Execute(hubArgs); err != nil { - return err - } +func (i *mockHubClient) Fork(gogh.Context, *gogh.Project, *gogh.Repo, bool, string, string) error { + return nil +} +func (i *mockHubClient) Create(gogh.Context, *gogh.Project, *gogh.Repo, string, *url.URL, bool, bool, bool) error { return nil } -func hubCreate( - ctx gogh.Context, - private bool, - description string, - homepage *url.URL, - browse bool, - clipboard bool, - repo *gogh.Repo, - directory string, -) (retErr error) { - tear, err := chdirTmp(directory) - if err != nil { - return err - } - defer func() { - if err := tear(); err != nil && retErr == nil { - retErr = err - } - }() +var defaultHubClient hubClient = &internal.HubClient{} - hubArgs := []string{"hub", "create"} - hubArgs = appendIf(hubArgs, "-p", private) - hubArgs = appendIf(hubArgs, "-o", browse) - hubArgs = appendIf(hubArgs, "-c", clipboard) - hubArgs = appendIfFilled(hubArgs, "-d", description) - if homepage != nil { - hubArgs = append(hubArgs, "-h", homepage.String()) - } - hubArgs = append(hubArgs, repo.FullName(ctx)) - log.Printf("debug: Calling `%s`", strings.Join(hubArgs, " ")) - if err := os.Setenv("GITHUB_HOST", repo.Host(ctx)); err != nil { - return err - } - if err := os.Setenv("GITHUB_TOKEN", ctx.GitHubToken()); err != nil { - return err - } - if err := commands.CmdRunner.Execute(hubArgs); err != nil { - return err - } - return nil +func hub() hubClient { + return defaultHubClient } diff --git a/command/internal/git.go b/command/internal/git.go new file mode 100644 index 00000000..7a009d27 --- /dev/null +++ b/command/internal/git.go @@ -0,0 +1,63 @@ +package internal + +import ( + "net/url" + "os" + "os/exec" + "path/filepath" + + "github.com/kyoh86/gogh/gogh" +) + +type GitClient struct{} + +func (c *GitClient) Init( + ctx gogh.Context, + project *gogh.Project, + bare bool, + template string, + separateGitDir string, + shared gogh.ProjectShared, +) error { + args := []string{"init"} + args = appendIf(args, "--bare", bare) + args = appendIfFilled(args, "--template", template) + args = appendIfFilled(args, "--separate-git-dir", separateGitDir) + args = appendIfFilled(args, "--shared", shared.String()) + args = append(args, project.FullPath) + cmd := exec.Command("git", args...) + cmd.Stdin = ctx.Stdin() + cmd.Stdout = ctx.Stdout() + cmd.Stderr = ctx.Stderr() + return execCommand(cmd) +} + +func (c *GitClient) Clone(ctx gogh.Context, project *gogh.Project, remote *url.URL, shallow bool) error { + dir, _ := filepath.Split(project.FullPath) + err := os.MkdirAll(dir, 0755) + if err != nil { + return err + } + + args := []string{"clone"} + if shallow { + args = append(args, "--depth", "1") + } + args = append(args, remote.String(), project.FullPath) + + cmd := exec.Command("git", args...) + cmd.Stdin = ctx.Stdin() + cmd.Stdout = ctx.Stdout() + cmd.Stderr = ctx.Stderr() + return execCommand(cmd) +} + +func (c *GitClient) Update(ctx gogh.Context, project *gogh.Project) error { + cmd := exec.Command("git", "pull", "--ff-only") + cmd.Stdin = ctx.Stdin() + cmd.Stdout = ctx.Stdout() + cmd.Stderr = ctx.Stderr() + cmd.Dir = project.FullPath + + return execCommand(cmd) +} diff --git a/command/internal/hub.go b/command/internal/hub.go new file mode 100644 index 00000000..1c9bc2cc --- /dev/null +++ b/command/internal/hub.go @@ -0,0 +1,107 @@ +package internal + +import ( + "log" + "net/url" + "os" + "strings" + + "github.com/github/hub/commands" + "github.com/kyoh86/gogh/gogh" +) + +func chdirTmp(dir string) (func() error, error) { + log.Printf("debug: Changing working directory to %s", dir) + cd, err := os.Getwd() + if err != nil { + return nil, err + } + if err := os.Chdir(dir); err != nil { + return nil, err + } + return func() error { + log.Printf("debug: Changing back working directory to %s", cd) + return os.Chdir(cd) + }, nil +} + +type HubClient struct{} + +func (c *HubClient) Fork( + ctx gogh.Context, + project *gogh.Project, + repo *gogh.Repo, + noRemote bool, + remoteName string, + organization string, +) (retErr error) { + tear, err := chdirTmp(project.FullPath) + if err != nil { + return err + } + defer func() { + if err := tear(); err != nil && retErr == nil { + retErr = err + } + }() + + hubArgs := []string{"hub", "fork"} + hubArgs = appendIf(hubArgs, "--no-remote", noRemote) + hubArgs = appendIfFilled(hubArgs, "--remote-name", remoteName) + hubArgs = appendIfFilled(hubArgs, "--organization", organization) + // call hub fork + if err := os.Setenv("GITHUB_HOST", repo.Host(ctx)); err != nil { + return err + } + if err := os.Setenv("GITHUB_TOKEN", ctx.GitHubToken()); err != nil { + return err + } + log.Printf("debug: Calling `%s`", strings.Join(hubArgs, " ")) + if err := commands.CmdRunner.Execute(hubArgs); err != nil { + return err + } + + return nil +} + +func (c *HubClient) Create( + ctx gogh.Context, + project *gogh.Project, + repo *gogh.Repo, + description string, + homepage *url.URL, + private, + browse, + clipboard bool, +) (retErr error) { + tear, err := chdirTmp(project.FullPath) + if err != nil { + return err + } + defer func() { + if err := tear(); err != nil && retErr == nil { + retErr = err + } + }() + + hubArgs := []string{"hub", "create"} + hubArgs = appendIf(hubArgs, "-p", private) + hubArgs = appendIf(hubArgs, "-o", browse) + hubArgs = appendIf(hubArgs, "-c", clipboard) + hubArgs = appendIfFilled(hubArgs, "-d", description) + if homepage != nil { + hubArgs = append(hubArgs, "-h", homepage.String()) + } + hubArgs = append(hubArgs, repo.FullName(ctx)) + log.Printf("debug: Calling `%s`", strings.Join(hubArgs, " ")) + if err := os.Setenv("GITHUB_HOST", repo.Host(ctx)); err != nil { + return err + } + if err := os.Setenv("GITHUB_TOKEN", ctx.GitHubToken()); err != nil { + return err + } + if err := commands.CmdRunner.Execute(hubArgs); err != nil { + return err + } + return nil +} diff --git a/command/param.go b/command/internal/param.go similarity index 94% rename from command/param.go rename to command/internal/param.go index 9a33b355..a7793c04 100644 --- a/command/param.go +++ b/command/internal/param.go @@ -1,4 +1,4 @@ -package command +package internal func appendIf(array []string, flag string, value bool) []string { if value { diff --git a/command/run.go b/command/internal/run.go similarity index 97% rename from command/run.go rename to command/internal/run.go index 94eb466c..a1307eb5 100644 --- a/command/run.go +++ b/command/internal/run.go @@ -1,4 +1,4 @@ -package command +package internal import ( "fmt" diff --git a/command/new.go b/command/new.go index c2969ae8..9589256d 100644 --- a/command/new.go +++ b/command/new.go @@ -39,11 +39,11 @@ func New( // git init log.Println("info: Initializing a repository") - if err := gitInit(ctx, bare, template, separateGitDir, shared, project.FullPath); err != nil { + if err := git().Init(ctx, project, bare, template, separateGitDir, shared); err != nil { return err } // hub create log.Println("info: Creating a new repository in GitHub") - return hubCreate(ctx, private, description, homepage, browse, clipboard, repo, project.FullPath) + return hub().Create(ctx, project, repo, description, homepage, private, browse, clipboard) } diff --git a/config/config.go b/config/config.go index 35150a25..d7928a82 100644 --- a/config/config.go +++ b/config/config.go @@ -31,6 +31,10 @@ type GitHubConfig struct { Host string `yaml:"host,omitempty" env:"GOGH_GITHUB_HOST"` } +func (c *Config) Stdin() io.Reader { + return os.Stdin +} + func (c *Config) Stdout() io.Writer { return os.Stdout } diff --git a/go.mod b/go.mod index 43c956f8..4494bb26 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/kyoh86/gogh require ( github.com/BurntSushi/toml v0.3.1 // indirect + github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 + github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 // indirect github.com/alecthomas/kingpin v0.0.0-20190313214811-7613e5d4efd9 + github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect github.com/atotto/clipboard v0.1.1 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c @@ -17,6 +20,7 @@ require ( github.com/mattn/go-isatty v0.0.4 // indirect github.com/mitchellh/go-homedir v1.0.0 // indirect github.com/pkg/errors v0.8.0 + github.com/sergi/go-diff v1.0.0 // indirect github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be diff --git a/go.sum b/go.sum index 92175f37..e7849953 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kingpin v0.0.0-20190313214811-7613e5d4efd9 h1:ThYLvDFpawmpvAijeMff6W8LoQT/v0ldT3d/qWuPVAk= github.com/alecthomas/kingpin v0.0.0-20190313214811-7613e5d4efd9/go.mod h1:idxgS9pV6OOpAhZvx+gcoGRMX9/tt0iqkw/pNxI0C14= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 h1:GDQdwm/gAcJcLAKQQZGOJ4knlw+7rfEQQcmwTbt4p5E= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= @@ -48,6 +54,8 @@ github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/gogh/context.go b/gogh/context.go index f8e93de3..def7d530 100644 --- a/gogh/context.go +++ b/gogh/context.go @@ -8,6 +8,7 @@ import ( // Context holds configurations and environments type Context interface { context.Context + Stdin() io.Reader Stdout() io.Writer Stderr() io.Writer GitHubUser() string diff --git a/gogh/context_test.go b/gogh/context_test.go deleted file mode 100644 index e330bc54..00000000 --- a/gogh/context_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package gogh - -import ( - "context" - "io" -) - -type implContext struct { - context.Context - stdout io.Writer - stderr io.Writer - gitHubUser string - gitHubToken string - gitHubHost string - logLevel string - logFlags int - logDate bool - logTime bool - logLongFile bool - logShortFile bool - logUTC bool - root []string -} - -func (c *implContext) Stdout() io.Writer { - return c.stdout -} - -func (c *implContext) Stderr() io.Writer { - return c.stderr -} - -func (c *implContext) GitHubUser() string { - return c.gitHubUser -} - -func (c *implContext) GitHubToken() string { - return c.gitHubToken -} - -func (c *implContext) GitHubHost() string { - return c.gitHubHost -} - -func (c *implContext) LogLevel() string { - return c.logLevel -} - -func (c *implContext) LogFlags() int { - return c.logFlags -} - -func (c *implContext) LogDate() bool { return c.logDate } -func (c *implContext) LogTime() bool { return c.logTime } -func (c *implContext) LogLongFile() bool { return c.logLongFile } -func (c *implContext) LogShortFile() bool { return c.logShortFile } -func (c *implContext) LogUTC() bool { return c.logUTC } - -func (c *implContext) Root() []string { - return c.root -} - -func (c *implContext) PrimaryRoot() string { - return c.root[0] -} diff --git a/gogh/formatter_test.go b/gogh/formatter_test.go index e1ec5e3c..8b26ee81 100644 --- a/gogh/formatter_test.go +++ b/gogh/formatter_test.go @@ -6,13 +6,14 @@ import ( "io/ioutil" "testing" + "github.com/kyoh86/gogh/internal/context" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestFormatter(t *testing.T) { t.Run("dry run formatters", func(t *testing.T) { - project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/gogh") + project, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/gogh") require.NoError(t, err) for _, f := range ProjectListFormats() { formatter, err := ProjectListFormat(f).Formatter() @@ -23,9 +24,9 @@ func TestFormatter(t *testing.T) { }) t.Run("rel path formatter", func(t *testing.T) { - project1, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") + project1, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) - project2, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") + project2, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") require.NoError(t, err) formatter, err := ProjectListFormatRelPath.Formatter() require.NoError(t, err) @@ -37,7 +38,7 @@ func TestFormatter(t *testing.T) { assert.Equal(t, `github.com/kyoh86/foo:github.com/kyoh86/bar:`, buf.String()) }) t.Run("writer error by rel path formatter", func(t *testing.T) { - project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") + project, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) formatter, err := ProjectListFormatRelPath.Formatter() require.NoError(t, err) @@ -46,9 +47,9 @@ func TestFormatter(t *testing.T) { }) t.Run("full path formatter", func(t *testing.T) { - project1, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") + project1, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) - project2, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") + project2, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") require.NoError(t, err) formatter, err := ProjectListFormatFullPath.Formatter() require.NoError(t, err) @@ -57,7 +58,7 @@ func TestFormatter(t *testing.T) { assert.Equal(t, 2, formatter.Len()) }) t.Run("writer error by full path formatter", func(t *testing.T) { - project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") + project, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) formatter, err := ProjectListFormatFullPath.Formatter() require.NoError(t, err) @@ -66,9 +67,9 @@ func TestFormatter(t *testing.T) { }) t.Run("url formatter", func(t *testing.T) { - project1, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") + project1, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) - project2, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") + project2, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") require.NoError(t, err) formatter, err := ProjectListFormatURL.Formatter() require.NoError(t, err) @@ -80,7 +81,7 @@ func TestFormatter(t *testing.T) { assert.Equal(t, `https://github.com/kyoh86/foo:https://github.com/kyoh86/bar:`, buf.String()) }) t.Run("writer error by url formatter", func(t *testing.T) { - project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") + project, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) formatter, err := ProjectListFormatURL.Formatter() require.NoError(t, err) @@ -89,17 +90,17 @@ func TestFormatter(t *testing.T) { }) t.Run("short formatter", func(t *testing.T) { - project1, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") + project1, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) - project2, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") + project2, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar") require.NoError(t, err) - project3, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh87/bar") + project3, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh87/bar") require.NoError(t, err) - project4, err := parseProject(&implContext{gitHubHost: "example.com"}, "/go/src", "/go/src/example.com/kyoh86/bar") + project4, err := parseProject(&context.MockContext{MGitHubHost: "example.com"}, "/go/src", "/go/src/example.com/kyoh86/bar") require.NoError(t, err) - project5, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/baz") + project5, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/baz") require.NoError(t, err) - project6, err := parseProject(&implContext{gitHubHost: "github.com"}, "/foo", "/foo/github.com/kyoh86/baz") + project6, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/foo", "/foo/github.com/kyoh86/baz") require.NoError(t, err) formatter, err := ProjectListFormatShort.Formatter() require.NoError(t, err) @@ -115,7 +116,7 @@ func TestFormatter(t *testing.T) { assert.Equal(t, `foo:github.com/kyoh86/bar:kyoh87/bar:example.com/kyoh86/bar:/go/src/github.com/kyoh86/baz:/foo/github.com/kyoh86/baz:`, buf.String()) }) t.Run("writer error by short formatter", func(t *testing.T) { - project, err := parseProject(&implContext{gitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") + project, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo") require.NoError(t, err) formatter, err := ProjectListFormatShort.Formatter() require.NoError(t, err) diff --git a/gogh/local_test.go b/gogh/local_test.go index 68987866..a0bf173d 100644 --- a/gogh/local_test.go +++ b/gogh/local_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/kyoh86/gogh/internal/context" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -14,7 +15,7 @@ import ( func TestParseProject(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - ctx := implContext{root: []string{tmp}, gitHubHost: "github.com"} + ctx := context.MockContext{MRoot: []string{tmp}, MGitHubHost: "github.com"} t.Run("in primary root", func(t *testing.T) { path := filepath.Join(tmp, "github.com", "kyoh86", "gogh") @@ -29,7 +30,7 @@ func TestParseProject(t *testing.T) { tmp2, err := ioutil.TempDir(os.TempDir(), "gogh-test2") require.NoError(t, err) ctx := ctx - ctx.root = append(ctx.root, tmp2) + ctx.MRoot = append(ctx.MRoot, tmp2) path := filepath.Join(tmp2, "github.com", "kyoh86", "gogh") p, err := parseProject(&ctx, tmp2, path) require.NoError(t, err) @@ -73,7 +74,7 @@ func TestParseProject(t *testing.T) { func TestFindOrNewProject(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - ctx := implContext{root: []string{tmp}, gitHubUser: "kyoh86", gitHubHost: "github.com"} + ctx := context.MockContext{MRoot: []string{tmp}, MGitHubUser: "kyoh86", MGitHubHost: "github.com"} path := filepath.Join(tmp, "github.com", "kyoh86", "gogh") @@ -100,7 +101,7 @@ func TestFindOrNewProject(t *testing.T) { assert.EqualError(t, err, `not supported host: "example.com"`) }) t.Run("fail with invalid root", func(t *testing.T) { - ctx := implContext{root: []string{"/\x00"}, gitHubUser: "kyoh86", gitHubHost: "github.com"} + ctx := context.MockContext{MRoot: []string{"/\x00"}, MGitHubUser: "kyoh86", MGitHubHost: "github.com"} _, err := FindOrNewProject(&ctx, parseURL(t, "ssh://git@github.com/kyoh86/gogh.git")) assert.Error(t, err) }) @@ -153,13 +154,13 @@ func TestWalk(t *testing.T) { } t.Run("Not existing root", func(t *testing.T) { t.Run("primary root", func(t *testing.T) { - ctx := implContext{root: []string{"/that/will/never/exist"}} + ctx := context.MockContext{MRoot: []string{"/that/will/never/exist"}} require.NoError(t, Walk(&ctx, neverCalled(t))) }) t.Run("secondary root", func(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - ctx := implContext{root: []string{tmp, "/that/will/never/exist"}} + ctx := context.MockContext{MRoot: []string{tmp, "/that/will/never/exist"}} require.NoError(t, Walk(&ctx, neverCalled(t))) }) }) @@ -169,7 +170,7 @@ func TestWalk(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) require.NoError(t, ioutil.WriteFile(filepath.Join(tmp, "foo"), nil, 0644)) - ctx := implContext{root: []string{filepath.Join(tmp, "foo")}} + ctx := context.MockContext{MRoot: []string{filepath.Join(tmp, "foo")}} require.NoError(t, Walk(&ctx, neverCalled(t))) require.NoError(t, WalkInPrimary(&ctx, neverCalled(t))) }) @@ -177,7 +178,7 @@ func TestWalk(t *testing.T) { tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) require.NoError(t, ioutil.WriteFile(filepath.Join(tmp, "foo"), nil, 0644)) - ctx := implContext{root: []string{tmp, filepath.Join(tmp, "foo")}} + ctx := context.MockContext{MRoot: []string{tmp, filepath.Join(tmp, "foo")}} require.NoError(t, Walk(&ctx, neverCalled(t))) require.NoError(t, WalkInPrimary(&ctx, neverCalled(t))) }) @@ -189,7 +190,7 @@ func TestWalk(t *testing.T) { path := filepath.Join(tmp, "github.com", "kyoh--86", "gogh") require.NoError(t, os.MkdirAll(filepath.Join(path, ".git"), 0755)) - ctx := implContext{root: []string{tmp, filepath.Join(tmp)}} + ctx := context.MockContext{MRoot: []string{tmp, filepath.Join(tmp)}} assert.NoError(t, Walk(&ctx, neverCalled(t))) }) @@ -200,7 +201,7 @@ func TestWalk(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(path, ".git"), 0755)) require.NoError(t, ioutil.WriteFile(filepath.Join(tmp, "foo"), nil, 0644)) - ctx := implContext{root: []string{tmp, filepath.Join(tmp, "foo")}, gitHubHost: "github.com"} + ctx := context.MockContext{MRoot: []string{tmp, filepath.Join(tmp, "foo")}, MGitHubHost: "github.com"} err = errors.New("sample error") assert.EqualError(t, Walk(&ctx, func(p *Project) error { assert.Equal(t, path, p.FullPath) @@ -217,7 +218,7 @@ func TestList_Symlink(t *testing.T) { symDir, err := ioutil.TempDir("", "") require.NoError(t, err) - ctx := &implContext{root: []string{root}, gitHubHost: "github.com"} + ctx := &context.MockContext{MRoot: []string{root}, MGitHubHost: "github.com"} err = os.MkdirAll(filepath.Join(root, "github.com", "atom", "atom", ".git"), 0777) require.NoError(t, err) @@ -251,7 +252,7 @@ func TestQuery(t *testing.T) { path5 := filepath.Join(root2, "github.com", "kyoh86", "gogh") require.NoError(t, os.MkdirAll(filepath.Join(path5, ".git"), 0755)) - ctx := implContext{root: []string{root1, root2}, gitHubHost: "github.com"} + ctx := context.MockContext{MRoot: []string{root1, root2}, MGitHubHost: "github.com"} assert.NoError(t, Query(&ctx, "never found", Walk, func(*Project) error { t.Fatal("should not be called but...") diff --git a/gogh/repo_test.go b/gogh/repo_test.go index dfa65028..b0dc5cfa 100644 --- a/gogh/repo_test.go +++ b/gogh/repo_test.go @@ -3,13 +3,14 @@ package gogh import ( "testing" + "github.com/kyoh86/gogh/internal/context" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRepo(t *testing.T) { t.Run("full HTTPS URL", func(t *testing.T) { - ctx := &implContext{} + ctx := &context.MockContext{} spec, err := ParseRepo("https://github.com/kyoh86/pusheen-explorer") require.NoError(t, err) assert.Equal(t, "kyoh86/pusheen-explorer", spec.FullName(ctx)) @@ -19,7 +20,7 @@ func TestRepo(t *testing.T) { }) t.Run("scp like URL 1", func(t *testing.T) { - ctx := &implContext{} + ctx := &context.MockContext{} spec, err := ParseRepo("git@github.com:kyoh86/pusheen-explorer.git") require.NoError(t, err) assert.Equal(t, "kyoh86/pusheen-explorer", spec.FullName(ctx)) @@ -28,7 +29,7 @@ func TestRepo(t *testing.T) { }) t.Run("scp like URL 2", func(t *testing.T) { - ctx := &implContext{} + ctx := &context.MockContext{} spec, err := ParseRepo("git@github.com:/kyoh86/pusheen-explorer.git") require.NoError(t, err) assert.Equal(t, "kyoh86/pusheen-explorer", spec.FullName(ctx)) @@ -37,7 +38,7 @@ func TestRepo(t *testing.T) { }) t.Run("scp like URL 3", func(t *testing.T) { - ctx := &implContext{} + ctx := &context.MockContext{} spec, err := ParseRepo("github.com:kyoh86/pusheen-explorer.git") require.NoError(t, err) assert.Equal(t, "kyoh86/pusheen-explorer", spec.FullName(ctx)) @@ -46,7 +47,7 @@ func TestRepo(t *testing.T) { }) t.Run("owner/name spec", func(t *testing.T) { - ctx := &implContext{gitHubHost: "github.com"} + ctx := &context.MockContext{MGitHubHost: "github.com"} spec, err := ParseRepo("kyoh86/gogh") require.NoError(t, err) assert.Equal(t, "kyoh86/gogh", spec.FullName(ctx)) @@ -55,7 +56,7 @@ func TestRepo(t *testing.T) { }) t.Run("name only spec", func(t *testing.T) { - ctx := &implContext{gitHubUser: "kyoh86", gitHubHost: "github.com"} + ctx := &context.MockContext{MGitHubUser: "kyoh86", MGitHubHost: "github.com"} spec, err := ParseRepo("gogh") require.NoError(t, err) assert.Equal(t, "kyoh86/gogh", spec.FullName(ctx)) @@ -146,15 +147,15 @@ func TestRepos(t *testing.T) { func TestCheckRepoHost(t *testing.T) { t.Run("valid GitHub URL", func(t *testing.T) { - ctx := implContext{gitHubHost: "github.com"} + ctx := context.MockContext{MGitHubHost: "github.com"} assert.NoError(t, CheckRepoHost(&ctx, parseURL(t, "https://github.com/kyoh86/gogh"))) }) t.Run("valid GitHub URL with trailing slashes", func(t *testing.T) { - ctx := implContext{gitHubHost: "github.com"} + ctx := context.MockContext{MGitHubHost: "github.com"} assert.NoError(t, CheckRepoHost(&ctx, parseURL(t, "https://github.com/kyoh86/gogh/"))) }) t.Run("not supported host URL", func(t *testing.T) { - ctx := implContext{gitHubHost: "github.com"} + ctx := context.MockContext{MGitHubHost: "github.com"} assert.EqualError(t, CheckRepoHost(&ctx, parseURL(t, "https://kyoh86.work/kyoh86/gogh")), `not supported host: "kyoh86.work"`) }) } diff --git a/gogh/validation_test.go b/gogh/validation_test.go index 621d04aa..f15c5b82 100644 --- a/gogh/validation_test.go +++ b/gogh/validation_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/kyoh86/gogh/internal/context" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -36,34 +37,34 @@ func TestValidateRoots(t *testing.T) { func TestValidateContext(t *testing.T) { t.Run("invalid root", func(t *testing.T) { - ctx := &implContext{ - root: []string{"/\x00"}, - logLevel: "warn", - gitHubUser: "kyoh86", + ctx := &context.MockContext{ + MRoot: []string{"/\x00"}, + MLogLevel: "warn", + MGitHubUser: "kyoh86", } assert.Error(t, ValidateContext(ctx)) }) t.Run("invalid owner", func(t *testing.T) { - ctx := &implContext{ - root: []string{"/path/to/not/existing"}, - logLevel: "warn", - gitHubUser: "", + ctx := &context.MockContext{ + MRoot: []string{"/path/to/not/existing"}, + MLogLevel: "warn", + MGitHubUser: "", } assert.Error(t, ValidateContext(ctx)) }) t.Run("invalid loglevel", func(t *testing.T) { - ctx := &implContext{ - root: []string{"/path/to/not/existing"}, - logLevel: "invalid", - gitHubUser: "kyoh86", + ctx := &context.MockContext{ + MRoot: []string{"/path/to/not/existing"}, + MLogLevel: "invalid", + MGitHubUser: "kyoh86", } assert.Error(t, ValidateContext(ctx)) }) t.Run("valid context", func(t *testing.T) { - ctx := &implContext{ - root: []string{"/path/to/not/existing"}, - logLevel: "warn", - gitHubUser: "kyoh86", + ctx := &context.MockContext{ + MRoot: []string{"/path/to/not/existing"}, + MLogLevel: "warn", + MGitHubUser: "kyoh86", } assert.NoError(t, ValidateContext(ctx)) }) diff --git a/internal/context/mock.go b/internal/context/mock.go new file mode 100644 index 00000000..5425ad3c --- /dev/null +++ b/internal/context/mock.go @@ -0,0 +1,70 @@ +package context + +import ( + "context" + "io" +) + +type MockContext struct { + context.Context + MStdin io.Reader + MStdout io.Writer + MStderr io.Writer + MGitHubUser string + MGitHubToken string + MGitHubHost string + MLogLevel string + MLogFlags int + MLogDate bool + MLogTime bool + MLogLongFile bool + MLogShortFile bool + MLogUTC bool + MRoot []string +} + +func (c *MockContext) Stdin() io.Reader { + return c.MStdin +} + +func (c *MockContext) Stdout() io.Writer { + return c.MStdout +} + +func (c *MockContext) Stderr() io.Writer { + return c.MStderr +} + +func (c *MockContext) GitHubUser() string { + return c.MGitHubUser +} + +func (c *MockContext) GitHubToken() string { + return c.MGitHubToken +} + +func (c *MockContext) GitHubHost() string { + return c.MGitHubHost +} + +func (c *MockContext) LogLevel() string { + return c.MLogLevel +} + +func (c *MockContext) LogFlags() int { + return c.MLogFlags +} + +func (c *MockContext) LogDate() bool { return c.MLogDate } +func (c *MockContext) LogTime() bool { return c.MLogTime } +func (c *MockContext) LogLongFile() bool { return c.MLogLongFile } +func (c *MockContext) LogShortFile() bool { return c.MLogShortFile } +func (c *MockContext) LogUTC() bool { return c.MLogUTC } + +func (c *MockContext) Root() []string { + return c.MRoot +} + +func (c *MockContext) PrimaryRoot() string { + return c.MRoot[0] +} From f9affb601c39efb5ba120b770e4a84a4da512542 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Thu, 28 Mar 2019 18:13:43 +0900 Subject: [PATCH 18/30] Empty command test --- command/empty_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 command/empty_test.go diff --git a/command/empty_test.go b/command/empty_test.go new file mode 100644 index 00000000..bcf5e771 --- /dev/null +++ b/command/empty_test.go @@ -0,0 +1,56 @@ +package command + +import ( + "bytes" + "io/ioutil" + "net/url" + "os" + "strings" + "testing" + + "github.com/alecthomas/assert" + "github.com/kyoh86/gogh/config" + "github.com/kyoh86/gogh/gogh" + "github.com/kyoh86/gogh/internal/context" + "github.com/stretchr/testify/require" +) + +func TestEmpty(t *testing.T) { + defaultGitClient = &mockGitClient{} + defaultHubClient = &mockHubClient{} + tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") + require.NoError(t, err) + ctx := &context.MockContext{ + MRoot: []string{tmp}, + MGitHubHost: "github.com", + MStdin: &bytes.Buffer{}, + MStdout: ioutil.Discard, + MStderr: ioutil.Discard, + } + + assert.NoError(t, Pipe(ctx, false, false, false, "echo", []string{"kyoh86/gogh"})) + ctx.MStdin = strings.NewReader(`kyoh86/gogh`) + assert.NoError(t, Bulk(ctx, false, false, false)) + mustRepo := func(name string) *gogh.Repo { + t.Helper() + repo, err := gogh.ParseRepo(name) + require.NoError(t, err) + return repo + } + assert.NoError(t, GetAll(ctx, false, false, false, gogh.Repos{ + *mustRepo("kyoh86/gogh"), + *mustRepo("kyoh86/vim-gogh"), + })) + assert.NoError(t, Get(ctx, false, false, false, mustRepo("kyoh86/gogh"))) + assert.NoError(t, ConfigGetAll(&config.Config{})) + assert.NoError(t, ConfigGet(&config.Config{}, "root")) + assert.NoError(t, ConfigPut(&config.Config{}, "root", "/tmp")) + assert.NoError(t, ConfigUnset(&config.Config{}, "root")) + assert.NoError(t, Fork(ctx, false, false, false, false, "", "", mustRepo("kyoh86/gogh"))) + assert.NoError(t, New(ctx, false, "", &url.URL{}, false, false, false, "", "", gogh.ProjectShared("false"), mustRepo("kyoh86/gogh"))) + // assert.NoError(t, Repos(ctx, "", true, false, false, "", "", "")) + assert.NoError(t, Setup(ctx, "gogogh", "zsh")) + assert.NoError(t, List(ctx, gogh.ProjectListFormatShort, false, "")) + assert.NoError(t, Where(ctx, false, false, "gogh")) + assert.NoError(t, Root(ctx, false)) +} From cf28aea0b58719e71c569016ebb76fe42867ff1e Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Fri, 29 Mar 2019 00:58:58 +0900 Subject: [PATCH 19/30] Refactor: cover commands --- command/config.go | 13 ++-- command/config_get_all_test.go | 40 +++++++++++ command/config_get_test.go | 23 +++++++ command/config_put_test.go | 15 +++++ command/config_test.go | 1 - command/config_unset_test.go | 20 ++++++ command/list_test.go | 62 +++++++++++++++++ command/remote/doc.go | 27 -------- config/accessor.go | 117 +++++++++++++++++++-------------- config/accessor_test.go | 82 +++++++++++------------ config/get.go | 8 +-- go.mod | 1 + go.sum | 2 + internal/util/util.go | 14 ---- 14 files changed, 281 insertions(+), 144 deletions(-) create mode 100644 command/config_get_all_test.go create mode 100644 command/config_get_test.go create mode 100644 command/config_put_test.go delete mode 100644 command/config_test.go create mode 100644 command/config_unset_test.go create mode 100644 command/list_test.go delete mode 100644 command/remote/doc.go delete mode 100644 internal/util/util.go diff --git a/command/config.go b/command/config.go index 65482bde..7f060426 100644 --- a/command/config.go +++ b/command/config.go @@ -7,9 +7,8 @@ import ( ) func ConfigGetAll(cfg *config.Config) error { - acc := config.DefaultAccessor - for _, name := range acc.OptionNames() { - opt, _ := acc.Option(name) // ignore error: acc.OptionNames covers all accessor + for _, name := range config.OptionNames() { + opt, _ := config.Option(name) // ignore error: config.OptionNames covers all accessor value := opt.Get(cfg) fmt.Printf("%s = %s\n", name, value) } @@ -17,17 +16,17 @@ func ConfigGetAll(cfg *config.Config) error { } func ConfigGet(cfg *config.Config, optionName string) error { - opt, err := config.DefaultAccessor.Option(optionName) + opt, err := config.Option(optionName) if err != nil { return err } value := opt.Get(cfg) - fmt.Printf("%s = %s\n", optionName, value) + fmt.Println(value) return nil } func ConfigPut(cfg *config.Config, optionName, optionValue string) error { - opt, err := config.DefaultAccessor.Option(optionName) + opt, err := config.Option(optionName) if err != nil { return err } @@ -35,7 +34,7 @@ func ConfigPut(cfg *config.Config, optionName, optionValue string) error { } func ConfigUnset(cfg *config.Config, optionName string) error { - opt, err := config.DefaultAccessor.Option(optionName) + opt, err := config.Option(optionName) if err != nil { return err } diff --git a/command/config_get_all_test.go b/command/config_get_all_test.go new file mode 100644 index 00000000..d689380d --- /dev/null +++ b/command/config_get_all_test.go @@ -0,0 +1,40 @@ +package command_test + +import ( + "github.com/kyoh86/gogh/command" + "github.com/kyoh86/gogh/config" +) + +func ExampleConfigGetAll() { + if err := command.ConfigGetAll(&config.Config{ + GitHub: config.GitHubConfig{ + Token: "tokenx1", + Host: "hostx1", + User: "kyoh86", + }, + Log: config.LogConfig{ + Level: "trace", + Date: config.TrueOption, + Time: config.FalseOption, + MicroSeconds: config.TrueOption, + LongFile: config.TrueOption, + ShortFile: config.TrueOption, + UTC: config.TrueOption, + }, + VRoot: []string{"/foo", "/bar"}, + }); err != nil { + panic(err) + } + // Unordered output: + // root = /foo:/bar + // github.host = hostx1 + // github.user = kyoh86 + // github.token = tokenx1 + // log.level = trace + // log.date = yes + // log.time = no + // log.microseconds = yes + // log.longfile = yes + // log.shortfile = yes + // log.utc = yes +} diff --git a/command/config_get_test.go b/command/config_get_test.go new file mode 100644 index 00000000..45d92366 --- /dev/null +++ b/command/config_get_test.go @@ -0,0 +1,23 @@ +package command_test + +import ( + "testing" + + "github.com/kyoh86/gogh/command" + "github.com/kyoh86/gogh/config" + "github.com/stretchr/testify/assert" +) + +func ExampleConfigGet() { + if err := command.ConfigGet(&config.Config{ + VRoot: []string{"/foo", "/bar"}, + }, "root"); err != nil { + panic(err) + } + // Output: + // /foo:/bar +} + +func TestConfigGet(t *testing.T) { + assert.EqualError(t, command.ConfigGet(&config.Config{}, "invalid.name"), "invalid option name") +} diff --git a/command/config_put_test.go b/command/config_put_test.go new file mode 100644 index 00000000..3961dc92 --- /dev/null +++ b/command/config_put_test.go @@ -0,0 +1,15 @@ +package command_test + +import ( + "testing" + + "github.com/kyoh86/gogh/command" + "github.com/kyoh86/gogh/config" + "github.com/stretchr/testify/assert" +) + +func TestConfigPut(t *testing.T) { + var cfg config.Config + assert.NoError(t, command.ConfigPut(&cfg, "github.host", "hostx1")) + assert.EqualError(t, command.ConfigPut(&cfg, "invalid.name", "hostx2"), "invalid option name") +} diff --git a/command/config_test.go b/command/config_test.go deleted file mode 100644 index d47dcf0d..00000000 --- a/command/config_test.go +++ /dev/null @@ -1 +0,0 @@ -package command diff --git a/command/config_unset_test.go b/command/config_unset_test.go new file mode 100644 index 00000000..829bb224 --- /dev/null +++ b/command/config_unset_test.go @@ -0,0 +1,20 @@ +package command_test + +import ( + "testing" + + "github.com/kyoh86/gogh/command" + "github.com/kyoh86/gogh/config" + "github.com/stretchr/testify/assert" +) + +func TestConfigUnset(t *testing.T) { + cfg := config.Config{ + GitHub: config.GitHubConfig{ + Host: "hostx1", + }, + } + assert.NoError(t, command.ConfigUnset(&cfg, "github.host")) + assert.Empty(t, cfg.GitHub.Host) + assert.EqualError(t, command.ConfigUnset(&cfg, "invalid.name"), "invalid option name") +} diff --git a/command/list_test.go b/command/list_test.go new file mode 100644 index 00000000..7617d5bd --- /dev/null +++ b/command/list_test.go @@ -0,0 +1,62 @@ +package command_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/alecthomas/assert" + "github.com/kyoh86/gogh/command" + "github.com/kyoh86/gogh/config" + "github.com/kyoh86/gogh/gogh" + "github.com/stretchr/testify/require" +) + +func ExampleList() { + tmp, _ := ioutil.TempDir(os.TempDir(), "gogh-test") + defer os.RemoveAll(tmp) + _ = os.MkdirAll(filepath.Join(tmp, "example.com", "kyoh86", "gogh", ".git"), 0755) + _ = os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "name", ".git"), 0755) + _ = os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "empty"), 0755) + + if err := command.List(&config.Config{ + VRoot: []string{tmp}, + GitHub: config.GitHubConfig{Host: "example.com"}, + }, + gogh.ProjectListFormatURL, + true, + "", + ); err != nil { + panic(err) + } + // Unordered output: + // https://example.com/kyoh86/gogh + // https://example.com/owner/name +} + +func TestList(t *testing.T) { + tmp, _ := ioutil.TempDir(os.TempDir(), "gogh-test") + defer os.RemoveAll(tmp) + require.NoError(t, os.MkdirAll(filepath.Join(tmp, "example.com", "kyoh86", "gogh", ".git"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "name", ".git"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "empty"), 0755)) + + assert.Error(t, command.List(&config.Config{ + VRoot: []string{tmp}, + GitHub: config.GitHubConfig{Host: "example.com"}, + }, + gogh.ProjectListFormat("invalid format"), + false, + "", + ), "invalid format") + + assert.Error(t, command.List(&config.Config{ + VRoot: []string{tmp, "/\x00"}, + GitHub: config.GitHubConfig{Host: "example.com"}, + }, + gogh.ProjectListFormatURL, + false, + "", + ), "invalid root") +} diff --git a/command/remote/doc.go b/command/remote/doc.go deleted file mode 100644 index 6bb1410f..00000000 --- a/command/remote/doc.go +++ /dev/null @@ -1,27 +0,0 @@ -/* -Package remote contains commands those get informations from GitHub. - -* Commands will be organized like below that follows GitHub API v3 specification. - See: https://developer.github.com/v3/ - -| Function | Subcommand | -|-------------------------|-------------------------------| -| List repos | repo | -| Search repos | search repo | -| Search commits | search commit | -| Search code | search code | -| Search pull requests | search issue | -| Search issues | search pr, pull, pull-request | -| Search users | search user | -| Search topics | search topic | -| Search labels | search label | - -All of plural form of each subcommand can be used. (i.e. "repos") - -* This should not manage pull-request and issues. -We should request features and enhancements to `hub` (https://github.com/github/hub). - -* This should not manage gists. - We should request features and enhancements to `gist` (http://defunkt.io/gist/). -*/ -package remote diff --git a/config/accessor.go b/config/accessor.go index 75dc90bb..87a18e08 100644 --- a/config/accessor.go +++ b/config/accessor.go @@ -6,31 +6,16 @@ import ( "strings" "github.com/kyoh86/gogh/gogh" - "github.com/kyoh86/gogh/internal/util" + "github.com/thoas/go-funk" ) -type Accessor map[string]OptionAccessor - var ( - DefaultAccessor = NewAccessor() + EmptyValue = errors.New("empty value") + RemoveFromMonoOption = errors.New("removing from mono option") + InvalidOptionName = errors.New("invalid option name") + TokenMustNotSave = errors.New("token must not save") ) -func (m Accessor) Option(optionName string) (*OptionAccessor, error) { - a, ok := m[optionName] - if !ok { - return nil, InvalidOptionName - } - return &a, nil -} - -func (m Accessor) OptionNames() []string { - arr := make([]string, 0, len(m)) - for o := range m { - arr = append(arr, o) - } - return arr -} - type OptionAccessor struct { optionName string getter func(cfg *Config) string @@ -43,33 +28,48 @@ func (a OptionAccessor) Put(cfg *Config, value string) error { return a.putter(c func (a OptionAccessor) Unset(cfg *Config) error { return a.unsetter(cfg) } var ( - EmptyValue = errors.New("empty value") - RemoveFromMonoOption = errors.New("removing from mono option") - InvalidOptionName = errors.New("invalid option name") - TokenMustNotSave = errors.New("token must not save") + configAccessor map[string]OptionAccessor + optionNames []string + optionAccessors = []OptionAccessor{ + rootOptionAccessor, + gitHubHostOptionAccessor, + gitHubUserOptionAccessor, + gitHubTokenOptionAccessor, + logLevelOptionAccessor, + logDateOptionAccessor, + logTimeOptionAccessor, + logMicroSecondsOptionAccessor, + logLongFileOptionAccessor, + logShortFileOptionAccessor, + logUTCOptionAccessor, + } ) -func NewAccessor() Accessor { - m := Accessor{} - for _, a := range []OptionAccessor{ - GitHubUserOptionAccessor, - GitHubTokenOptionAccessor, - GitHubHostOptionAccessor, - LogLevelOptionAccessor, - LogDateOptionAccessor, - LogTimeOptionAccessor, - LogLongFileOptionAccessor, - LogShortFileOptionAccessor, - LogUTCOptionAccessor, - RootOptionAccessor, - } { +func init() { + m := map[string]OptionAccessor{} + n := make([]string, 0, len(optionAccessors)) + for _, a := range optionAccessors { + n = append(n, a.optionName) m[a.optionName] = a } - return m + configAccessor = m + optionNames = n +} + +func Option(optionName string) (*OptionAccessor, error) { + a, ok := configAccessor[optionName] + if !ok { + return nil, InvalidOptionName + } + return &a, nil +} + +func OptionNames() []string { + return optionNames } var ( - GitHubUserOptionAccessor = OptionAccessor{ + gitHubUserOptionAccessor = OptionAccessor{ optionName: "github.user", getter: func(cfg *Config) string { return cfg.GitHubUser() @@ -90,7 +90,7 @@ var ( }, } - GitHubTokenOptionAccessor = OptionAccessor{ + gitHubTokenOptionAccessor = OptionAccessor{ optionName: "github.token", getter: func(cfg *Config) string { return cfg.GitHubToken() @@ -104,7 +104,7 @@ var ( }, } - GitHubHostOptionAccessor = OptionAccessor{ + gitHubHostOptionAccessor = OptionAccessor{ optionName: "github.host", getter: func(cfg *Config) string { return cfg.GitHubHost() @@ -122,7 +122,7 @@ var ( }, } - LogLevelOptionAccessor = OptionAccessor{ + logLevelOptionAccessor = OptionAccessor{ optionName: "log.level", getter: func(cfg *Config) string { return cfg.LogLevel() @@ -143,7 +143,7 @@ var ( }, } - LogDateOptionAccessor = OptionAccessor{ + logDateOptionAccessor = OptionAccessor{ optionName: "log.date", getter: func(cfg *Config) string { return cfg.Log.Date.String() @@ -160,7 +160,7 @@ var ( }, } - LogTimeOptionAccessor = OptionAccessor{ + logTimeOptionAccessor = OptionAccessor{ optionName: "log.time", getter: func(cfg *Config) string { return cfg.Log.Time.String() @@ -177,7 +177,24 @@ var ( }, } - LogLongFileOptionAccessor = OptionAccessor{ + logMicroSecondsOptionAccessor = OptionAccessor{ + optionName: "log.microseconds", + getter: func(cfg *Config) string { + return cfg.Log.MicroSeconds.String() + }, + putter: func(cfg *Config, value string) error { + if value == "" { + return EmptyValue + } + return cfg.Log.MicroSeconds.Decode(value) + }, + unsetter: func(cfg *Config) error { + cfg.Log.MicroSeconds = EmptyBoolOption + return nil + }, + } + + logLongFileOptionAccessor = OptionAccessor{ optionName: "log.longfile", getter: func(cfg *Config) string { return cfg.Log.LongFile.String() @@ -194,7 +211,7 @@ var ( }, } - LogShortFileOptionAccessor = OptionAccessor{ + logShortFileOptionAccessor = OptionAccessor{ optionName: "log.shortfile", getter: func(cfg *Config) string { return cfg.Log.ShortFile.String() @@ -211,7 +228,7 @@ var ( }, } - LogUTCOptionAccessor = OptionAccessor{ + logUTCOptionAccessor = OptionAccessor{ optionName: "log.utc", getter: func(cfg *Config) string { return cfg.Log.UTC.String() @@ -228,7 +245,7 @@ var ( }, } - RootOptionAccessor = OptionAccessor{ + rootOptionAccessor = OptionAccessor{ optionName: "root", getter: func(cfg *Config) string { return strings.Join(cfg.Root(), string(filepath.ListSeparator)) @@ -243,7 +260,7 @@ var ( if err := gogh.ValidateRoots(list); err != nil { return err } - cfg.VRoot = util.UniqueStringArray(append(cfg.VRoot, list...)) + cfg.VRoot = funk.UniqString(append(cfg.VRoot, list...)) return nil }, unsetter: func(cfg *Config) error { diff --git a/config/accessor_test.go b/config/accessor_test.go index 01e11e2c..5a0e84b4 100644 --- a/config/accessor_test.go +++ b/config/accessor_test.go @@ -26,18 +26,18 @@ func TestAccessor(t *testing.T) { cfg.Log.UTC = TrueOption cfg.VRoot = []string{"/foo", "/bar"} - _, err := DefaultAccessor.Option("invalid name") + _, err := Option("invalid name") assert.EqualError(t, err, "invalid option name") - assert.Equal(t, "token1", mustOption(DefaultAccessor.Option("github.token")).Get(&cfg)) - assert.Equal(t, "hostx1", mustOption(DefaultAccessor.Option("github.host")).Get(&cfg)) - assert.Equal(t, "kyoh86", mustOption(DefaultAccessor.Option("github.user")).Get(&cfg)) - assert.Equal(t, "trace", mustOption(DefaultAccessor.Option("log.level")).Get(&cfg)) - assert.Equal(t, "yes", mustOption(DefaultAccessor.Option("log.date")).Get(&cfg)) - assert.Equal(t, "no", mustOption(DefaultAccessor.Option("log.time")).Get(&cfg)) - assert.Equal(t, "yes", mustOption(DefaultAccessor.Option("log.longfile")).Get(&cfg)) - assert.Equal(t, "yes", mustOption(DefaultAccessor.Option("log.shortfile")).Get(&cfg)) - assert.Equal(t, "yes", mustOption(DefaultAccessor.Option("log.utc")).Get(&cfg)) - assert.Equal(t, "/foo:/bar", mustOption(DefaultAccessor.Option("root")).Get(&cfg)) + assert.Equal(t, "token1", mustOption(Option("github.token")).Get(&cfg)) + assert.Equal(t, "hostx1", mustOption(Option("github.host")).Get(&cfg)) + assert.Equal(t, "kyoh86", mustOption(Option("github.user")).Get(&cfg)) + assert.Equal(t, "trace", mustOption(Option("log.level")).Get(&cfg)) + assert.Equal(t, "yes", mustOption(Option("log.date")).Get(&cfg)) + assert.Equal(t, "no", mustOption(Option("log.time")).Get(&cfg)) + assert.Equal(t, "yes", mustOption(Option("log.longfile")).Get(&cfg)) + assert.Equal(t, "yes", mustOption(Option("log.shortfile")).Get(&cfg)) + assert.Equal(t, "yes", mustOption(Option("log.utc")).Get(&cfg)) + assert.Equal(t, "/foo:/bar", mustOption(Option("root")).Get(&cfg)) }) t.Run("putting", func(t *testing.T) { mustOption := func(acc *OptionAccessor, err error) *OptionAccessor { @@ -46,15 +46,15 @@ func TestAccessor(t *testing.T) { return acc } var cfg Config - assert.NoError(t, mustOption(DefaultAccessor.Option("github.host")).Put(&cfg, "hostx1")) - assert.NoError(t, mustOption(DefaultAccessor.Option("github.user")).Put(&cfg, "kyoh86")) - assert.NoError(t, mustOption(DefaultAccessor.Option("log.level")).Put(&cfg, "trace")) - assert.NoError(t, mustOption(DefaultAccessor.Option("log.date")).Put(&cfg, "yes")) - assert.NoError(t, mustOption(DefaultAccessor.Option("log.time")).Put(&cfg, "no")) - assert.NoError(t, mustOption(DefaultAccessor.Option("log.longfile")).Put(&cfg, "yes")) - assert.NoError(t, mustOption(DefaultAccessor.Option("log.shortfile")).Put(&cfg, "yes")) - assert.NoError(t, mustOption(DefaultAccessor.Option("log.utc")).Put(&cfg, "yes")) - assert.NoError(t, mustOption(DefaultAccessor.Option("root")).Put(&cfg, "/foo:/bar")) + assert.NoError(t, mustOption(Option("github.host")).Put(&cfg, "hostx1")) + assert.NoError(t, mustOption(Option("github.user")).Put(&cfg, "kyoh86")) + assert.NoError(t, mustOption(Option("log.level")).Put(&cfg, "trace")) + assert.NoError(t, mustOption(Option("log.date")).Put(&cfg, "yes")) + assert.NoError(t, mustOption(Option("log.time")).Put(&cfg, "no")) + assert.NoError(t, mustOption(Option("log.longfile")).Put(&cfg, "yes")) + assert.NoError(t, mustOption(Option("log.shortfile")).Put(&cfg, "yes")) + assert.NoError(t, mustOption(Option("log.utc")).Put(&cfg, "yes")) + assert.NoError(t, mustOption(Option("root")).Put(&cfg, "/foo:/bar")) assert.Equal(t, "", cfg.GitHub.Token) assert.Equal(t, "hostx1", cfg.GitHub.Host) @@ -79,26 +79,26 @@ func TestAccessor(t *testing.T) { return acc } var cfg Config - assert.EqualError(t, mustOption(DefaultAccessor.Option("github.token")).Put(&cfg, "token1"), "token must not save") + assert.EqualError(t, mustOption(Option("github.token")).Put(&cfg, "token1"), "token must not save") - assert.EqualError(t, mustOption(DefaultAccessor.Option("github.host")).Put(&cfg, ""), "empty value") - assert.EqualError(t, mustOption(DefaultAccessor.Option("github.user")).Put(&cfg, ""), "empty value") - assert.EqualError(t, mustOption(DefaultAccessor.Option("log.level")).Put(&cfg, ""), "empty value") - assert.EqualError(t, mustOption(DefaultAccessor.Option("log.date")).Put(&cfg, ""), "empty value") - assert.EqualError(t, mustOption(DefaultAccessor.Option("log.time")).Put(&cfg, ""), "empty value") - assert.EqualError(t, mustOption(DefaultAccessor.Option("log.longfile")).Put(&cfg, ""), "empty value") - assert.EqualError(t, mustOption(DefaultAccessor.Option("log.shortfile")).Put(&cfg, ""), "empty value") - assert.EqualError(t, mustOption(DefaultAccessor.Option("log.utc")).Put(&cfg, ""), "empty value") - assert.EqualError(t, mustOption(DefaultAccessor.Option("root")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("github.host")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("github.user")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("log.level")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("log.date")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("log.time")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("log.longfile")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("log.shortfile")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("log.utc")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("root")).Put(&cfg, ""), "empty value") - assert.Error(t, mustOption(DefaultAccessor.Option("github.user")).Put(&cfg, "-kyoh86"), "invalid github username") - assert.Error(t, mustOption(DefaultAccessor.Option("log.level")).Put(&cfg, "foobar"), "invalid log level") - assert.Error(t, mustOption(DefaultAccessor.Option("log.date")).Put(&cfg, "invalid value"), "invalid value") - assert.Error(t, mustOption(DefaultAccessor.Option("log.time")).Put(&cfg, "invalid value"), "invalid value") - assert.Error(t, mustOption(DefaultAccessor.Option("log.longfile")).Put(&cfg, "invalid value"), "invalid value") - assert.Error(t, mustOption(DefaultAccessor.Option("log.shortfile")).Put(&cfg, "invalid value"), "invalid value") - assert.Error(t, mustOption(DefaultAccessor.Option("log.utc")).Put(&cfg, "invalid value"), "invalid value") - assert.Error(t, mustOption(DefaultAccessor.Option("root")).Put(&cfg, "\x00"), "invalid value") + assert.Error(t, mustOption(Option("github.user")).Put(&cfg, "-kyoh86"), "invalid github username") + assert.Error(t, mustOption(Option("log.level")).Put(&cfg, "foobar"), "invalid log level") + assert.Error(t, mustOption(Option("log.date")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(Option("log.time")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(Option("log.longfile")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(Option("log.shortfile")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(Option("log.utc")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(Option("root")).Put(&cfg, "\x00"), "invalid value") assert.Equal(t, "", cfg.GitHub.Token) assert.Equal(t, "", cfg.GitHub.Host) @@ -125,10 +125,10 @@ func TestAccessor(t *testing.T) { cfg.Log.UTC = TrueOption cfg.VRoot = []string{"/foo", "/bar"} - _, err := DefaultAccessor.Option("invalid name") + _, err := Option("invalid name") assert.EqualError(t, err, "invalid option name") - for _, name := range DefaultAccessor.OptionNames() { - acc, err := DefaultAccessor.Option(name) + for _, name := range OptionNames() { + acc, err := Option(name) require.NoError(t, err) assert.NoError(t, acc.Unset(&cfg), name) } diff --git a/config/get.go b/config/get.go index 6ce95099..45ebeeee 100644 --- a/config/get.go +++ b/config/get.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/joeshaw/envdecode" - "github.com/kyoh86/gogh/internal/util" + "github.com/thoas/go-funk" yaml "gopkg.in/yaml.v2" ) @@ -63,7 +63,7 @@ func DefaultConfig() *Config { for _, gopath := range gopaths { root = append(root, filepath.Join(gopath, "src")) } - defaultConfig.VRoot = util.UniqueStringArray(root) + defaultConfig.VRoot = funk.UniqString(root) }) return &defaultConfig } @@ -73,7 +73,7 @@ func LoadConfig(r io.Reader) (config *Config, err error) { if err := yaml.NewDecoder(r).Decode(config); err != nil { return nil, err } - config.VRoot = util.UniqueStringArray(config.VRoot) + config.VRoot = funk.UniqString(config.VRoot) return } @@ -87,6 +87,6 @@ func GetEnvarConfig() (config *Config, err error) { if err == envdecode.ErrNoTargetFieldsAreSet { err = nil } - config.VRoot = util.UniqueStringArray(config.VRoot) + config.VRoot = funk.UniqString(config.VRoot) return } diff --git a/go.mod b/go.mod index 4494bb26..cad34a73 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/pkg/errors v0.8.0 github.com/sergi/go-diff v1.0.0 // indirect github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 + github.com/thoas/go-funk v0.0.0-20190328084834-b6996520715f golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect diff --git a/go.sum b/go.sum index e7849953..e0e8e240 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 h1:Ko2LQMrRU+Oy/+EDBwX7eZ2jp3C47eDBB8EIhKTun+I= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/thoas/go-funk v0.0.0-20190328084834-b6996520715f h1:hucXnlEXfHvrY7vI9hP1bJX/olUUt5a6u3QvxXrtANw= +github.com/thoas/go-funk v0.0.0-20190328084834-b6996520715f/go.mod h1:mlR+dHGb+4YgXkf13rkQTuzrneeHANxOm6+ZnEV9HsA= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/internal/util/util.go b/internal/util/util.go deleted file mode 100644 index 5084f493..00000000 --- a/internal/util/util.go +++ /dev/null @@ -1,14 +0,0 @@ -package util - -func UniqueStringArray(items []string) (uniq []string) { - dups := map[string]struct{}{} - - for _, item := range items { - if _, ok := dups[item]; ok { - continue - } - dups[item] = struct{}{} - uniq = append(uniq, item) - } - return -} From 64538739d223c80ae505e60c1bc6fb93d33a7777 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Fri, 29 Mar 2019 08:23:19 +0900 Subject: [PATCH 20/30] Cover config.Config.Log.MicroSeconds --- config/accessor_test.go | 10 ++++++++++ config/config.go | 11 ++++++----- config/get_test.go | 1 + gogh/context.go | 1 + internal/context/mock.go | 40 +++++++++++++++++++++------------------- 5 files changed, 39 insertions(+), 24 deletions(-) diff --git a/config/accessor_test.go b/config/accessor_test.go index 5a0e84b4..bd23b8bb 100644 --- a/config/accessor_test.go +++ b/config/accessor_test.go @@ -21,6 +21,7 @@ func TestAccessor(t *testing.T) { cfg.Log.Level = "trace" cfg.Log.Date = TrueOption cfg.Log.Time = FalseOption + cfg.Log.MicroSeconds = TrueOption cfg.Log.LongFile = TrueOption cfg.Log.ShortFile = TrueOption cfg.Log.UTC = TrueOption @@ -34,6 +35,7 @@ func TestAccessor(t *testing.T) { assert.Equal(t, "trace", mustOption(Option("log.level")).Get(&cfg)) assert.Equal(t, "yes", mustOption(Option("log.date")).Get(&cfg)) assert.Equal(t, "no", mustOption(Option("log.time")).Get(&cfg)) + assert.Equal(t, "yes", mustOption(Option("log.microseconds")).Get(&cfg)) assert.Equal(t, "yes", mustOption(Option("log.longfile")).Get(&cfg)) assert.Equal(t, "yes", mustOption(Option("log.shortfile")).Get(&cfg)) assert.Equal(t, "yes", mustOption(Option("log.utc")).Get(&cfg)) @@ -51,6 +53,7 @@ func TestAccessor(t *testing.T) { assert.NoError(t, mustOption(Option("log.level")).Put(&cfg, "trace")) assert.NoError(t, mustOption(Option("log.date")).Put(&cfg, "yes")) assert.NoError(t, mustOption(Option("log.time")).Put(&cfg, "no")) + assert.NoError(t, mustOption(Option("log.microseconds")).Put(&cfg, "yes")) assert.NoError(t, mustOption(Option("log.longfile")).Put(&cfg, "yes")) assert.NoError(t, mustOption(Option("log.shortfile")).Put(&cfg, "yes")) assert.NoError(t, mustOption(Option("log.utc")).Put(&cfg, "yes")) @@ -64,6 +67,8 @@ func TestAccessor(t *testing.T) { assert.True(t, cfg.LogDate()) assert.Equal(t, FalseOption, cfg.Log.Time) assert.False(t, cfg.LogTime()) + assert.Equal(t, TrueOption, cfg.Log.MicroSeconds) + assert.True(t, cfg.LogMicroSeconds()) assert.Equal(t, TrueOption, cfg.Log.LongFile) assert.True(t, cfg.LogLongFile()) assert.Equal(t, TrueOption, cfg.Log.ShortFile) @@ -86,6 +91,7 @@ func TestAccessor(t *testing.T) { assert.EqualError(t, mustOption(Option("log.level")).Put(&cfg, ""), "empty value") assert.EqualError(t, mustOption(Option("log.date")).Put(&cfg, ""), "empty value") assert.EqualError(t, mustOption(Option("log.time")).Put(&cfg, ""), "empty value") + assert.EqualError(t, mustOption(Option("log.microseconds")).Put(&cfg, ""), "empty value") assert.EqualError(t, mustOption(Option("log.longfile")).Put(&cfg, ""), "empty value") assert.EqualError(t, mustOption(Option("log.shortfile")).Put(&cfg, ""), "empty value") assert.EqualError(t, mustOption(Option("log.utc")).Put(&cfg, ""), "empty value") @@ -95,6 +101,7 @@ func TestAccessor(t *testing.T) { assert.Error(t, mustOption(Option("log.level")).Put(&cfg, "foobar"), "invalid log level") assert.Error(t, mustOption(Option("log.date")).Put(&cfg, "invalid value"), "invalid value") assert.Error(t, mustOption(Option("log.time")).Put(&cfg, "invalid value"), "invalid value") + assert.Error(t, mustOption(Option("log.microseconds")).Put(&cfg, "invalid value"), "invalid value") assert.Error(t, mustOption(Option("log.longfile")).Put(&cfg, "invalid value"), "invalid value") assert.Error(t, mustOption(Option("log.shortfile")).Put(&cfg, "invalid value"), "invalid value") assert.Error(t, mustOption(Option("log.utc")).Put(&cfg, "invalid value"), "invalid value") @@ -106,6 +113,7 @@ func TestAccessor(t *testing.T) { assert.Equal(t, "", cfg.Log.Level) assert.Equal(t, EmptyBoolOption, cfg.Log.Date) assert.Equal(t, EmptyBoolOption, cfg.Log.Time) + assert.Equal(t, EmptyBoolOption, cfg.Log.MicroSeconds) assert.Equal(t, EmptyBoolOption, cfg.Log.LongFile) assert.Equal(t, EmptyBoolOption, cfg.Log.ShortFile) assert.Equal(t, EmptyBoolOption, cfg.Log.UTC) @@ -120,6 +128,7 @@ func TestAccessor(t *testing.T) { cfg.Log.Level = "trace" cfg.Log.Date = TrueOption cfg.Log.Time = FalseOption + cfg.Log.MicroSeconds = TrueOption cfg.Log.LongFile = TrueOption cfg.Log.ShortFile = TrueOption cfg.Log.UTC = TrueOption @@ -138,6 +147,7 @@ func TestAccessor(t *testing.T) { assert.Equal(t, "", cfg.Log.Level) assert.Equal(t, EmptyBoolOption, cfg.Log.Date) assert.Equal(t, EmptyBoolOption, cfg.Log.Time) + assert.Equal(t, EmptyBoolOption, cfg.Log.MicroSeconds) assert.Equal(t, EmptyBoolOption, cfg.Log.LongFile) assert.Equal(t, EmptyBoolOption, cfg.Log.ShortFile) assert.Equal(t, EmptyBoolOption, cfg.Log.UTC) diff --git a/config/config.go b/config/config.go index d7928a82..d3ffeabd 100644 --- a/config/config.go +++ b/config/config.go @@ -82,11 +82,12 @@ func (c *Config) LogFlags() int { return f } -func (c *Config) LogDate() bool { return c.Log.Date.Bool() } -func (c *Config) LogTime() bool { return c.Log.Time.Bool() } -func (c *Config) LogLongFile() bool { return c.Log.LongFile.Bool() } -func (c *Config) LogShortFile() bool { return c.Log.ShortFile.Bool() } -func (c *Config) LogUTC() bool { return c.Log.UTC.Bool() } +func (c *Config) LogDate() bool { return c.Log.Date.Bool() } +func (c *Config) LogTime() bool { return c.Log.Time.Bool() } +func (c *Config) LogMicroSeconds() bool { return c.Log.MicroSeconds.Bool() } +func (c *Config) LogLongFile() bool { return c.Log.LongFile.Bool() } +func (c *Config) LogShortFile() bool { return c.Log.ShortFile.Bool() } +func (c *Config) LogUTC() bool { return c.Log.UTC.Bool() } func (c *Config) Root() []string { return c.VRoot diff --git a/config/get_test.go b/config/get_test.go index 1b44c7e6..ee0b37c8 100644 --- a/config/get_test.go +++ b/config/get_test.go @@ -28,6 +28,7 @@ func TestDefaultConfig(t *testing.T) { assert.NotEmpty(t, cfg.PrimaryRoot()) assert.Equal(t, os.Stderr, cfg.Stderr()) assert.Equal(t, os.Stdout, cfg.Stdout()) + assert.Equal(t, os.Stdin, cfg.Stdin()) } func TestLoadConfig(t *testing.T) { diff --git a/gogh/context.go b/gogh/context.go index def7d530..1ae25760 100644 --- a/gogh/context.go +++ b/gogh/context.go @@ -18,6 +18,7 @@ type Context interface { LogFlags() int // log.Lxxx flags LogDate() bool LogTime() bool + LogMicroSeconds() bool LogLongFile() bool LogShortFile() bool LogUTC() bool diff --git a/internal/context/mock.go b/internal/context/mock.go index 5425ad3c..3119735c 100644 --- a/internal/context/mock.go +++ b/internal/context/mock.go @@ -7,20 +7,21 @@ import ( type MockContext struct { context.Context - MStdin io.Reader - MStdout io.Writer - MStderr io.Writer - MGitHubUser string - MGitHubToken string - MGitHubHost string - MLogLevel string - MLogFlags int - MLogDate bool - MLogTime bool - MLogLongFile bool - MLogShortFile bool - MLogUTC bool - MRoot []string + MStdin io.Reader + MStdout io.Writer + MStderr io.Writer + MGitHubUser string + MGitHubToken string + MGitHubHost string + MLogLevel string + MLogFlags int + MLogDate bool + MLogTime bool + MLogMicroSeconds bool + MLogLongFile bool + MLogShortFile bool + MLogUTC bool + MRoot []string } func (c *MockContext) Stdin() io.Reader { @@ -55,11 +56,12 @@ func (c *MockContext) LogFlags() int { return c.MLogFlags } -func (c *MockContext) LogDate() bool { return c.MLogDate } -func (c *MockContext) LogTime() bool { return c.MLogTime } -func (c *MockContext) LogLongFile() bool { return c.MLogLongFile } -func (c *MockContext) LogShortFile() bool { return c.MLogShortFile } -func (c *MockContext) LogUTC() bool { return c.MLogUTC } +func (c *MockContext) LogDate() bool { return c.MLogDate } +func (c *MockContext) LogTime() bool { return c.MLogTime } +func (c *MockContext) LogMicroSeconds() bool { return c.MLogMicroSeconds } +func (c *MockContext) LogLongFile() bool { return c.MLogLongFile } +func (c *MockContext) LogShortFile() bool { return c.MLogShortFile } +func (c *MockContext) LogUTC() bool { return c.MLogUTC } func (c *MockContext) Root() []string { return c.MRoot From 16905e3221cfd80f5f6dce947413557fdeb5080f Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 00:17:19 +0900 Subject: [PATCH 21/30] try: remove error check for fmt.Fprintln --- command/fork.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/command/fork.go b/command/fork.go index 02d91647..c720601c 100644 --- a/command/fork.go +++ b/command/fork.go @@ -19,8 +19,6 @@ func Fork(ctx gogh.Context, update, withSSH, shallow, noRemote bool, remoteName return err } - if _, err := fmt.Fprintln(ctx.Stdout(), project.RelPath); err != nil { - return err - } + fmt.Fprintln(ctx.Stdout(), project.RelPath) return nil } From 33c9807b9e36d1875994622bb91042a5411dbd8d Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 00:20:25 +0900 Subject: [PATCH 22/30] remove error check for fmt.Fprint* --- command/root.go | 8 +++----- command/setup.go | 16 ++++------------ gogh/formatter.go | 16 ++++------------ 3 files changed, 11 insertions(+), 29 deletions(-) diff --git a/command/root.go b/command/root.go index a2870acc..5f67a717 100644 --- a/command/root.go +++ b/command/root.go @@ -10,14 +10,12 @@ import ( // Root prints a gogh.root func Root(ctx gogh.Context, all bool) error { if !all { - _, err := fmt.Fprintln(ctx.Stdout(), ctx.PrimaryRoot()) - return err + fmt.Fprintln(ctx.Stdout(), ctx.PrimaryRoot()) + return nil } log.Println("info: Finding all roots...") for _, root := range ctx.Root() { - if _, err := fmt.Fprintln(ctx.Stdout(), root); err != nil { - return err - } + fmt.Fprintln(ctx.Stdout(), root) } return nil } diff --git a/command/setup.go b/command/setup.go index f050f355..d423debb 100644 --- a/command/setup.go +++ b/command/setup.go @@ -13,20 +13,12 @@ func Setup(ctx gogh.Context, cdFuncName, shell string) error { _, shName := filepath.Split(shell) switch shName { case "zsh": - if _, err := fmt.Fprintf(ctx.Stdout(), `function %s { cd $(gogh find $@) }%s`, cdFuncName, "\n"); err != nil { - return err - } - if _, err := fmt.Fprintf(ctx.Stdout(), `eval "$(gogh --completion-script-zsh)"%s`, "\n"); err != nil { - return err - } + fmt.Fprintf(ctx.Stdout(), `function %s { cd $(gogh find $@) }%s`, cdFuncName, "\n") + fmt.Fprintf(ctx.Stdout(), `eval "$(gogh --completion-script-zsh)"%s`, "\n") return nil case "bash": - if _, err := fmt.Fprintf(ctx.Stdout(), `function %s { cd $(gogh find $@) }%s`, cdFuncName, "\n"); err != nil { - return err - } - if _, err := fmt.Fprintf(ctx.Stdout(), `eval "$(gogh --completion-script-bash)"%s`, "\n"); err != nil { - return err - } + fmt.Fprintf(ctx.Stdout(), `function %s { cd $(gogh find $@) }%s`, cdFuncName, "\n") + fmt.Fprintf(ctx.Stdout(), `eval "$(gogh --completion-script-bash)"%s`, "\n") return nil default: return fmt.Errorf("unsupported shell %q", shell) diff --git a/gogh/formatter.go b/gogh/formatter.go index dcd08249..0d485746 100644 --- a/gogh/formatter.go +++ b/gogh/formatter.go @@ -94,9 +94,7 @@ func (f *shortListFormatter) Len() int { func (f *shortListFormatter) PrintAll(w io.Writer, sep string) error { for _, project := range f.list { - if _, err := fmt.Fprint(w, f.shortName(project)+sep); err != nil { - return err - } + fmt.Fprint(w, f.shortName(project)+sep) } return nil } @@ -129,9 +127,7 @@ type fullPathFormatter struct { func (f *fullPathFormatter) PrintAll(w io.Writer, sep string) error { for _, project := range f.list { - if _, err := fmt.Fprint(w, project.FullPath+sep); err != nil { - return err - } + fmt.Fprint(w, project.FullPath+sep) } return nil } @@ -142,9 +138,7 @@ type urlFormatter struct { func (f *urlFormatter) PrintAll(w io.Writer, sep string) error { for _, project := range f.list { - if _, err := fmt.Fprint(w, "https://"+project.RelPath+sep); err != nil { - return err - } + fmt.Fprint(w, "https://"+project.RelPath+sep) } return nil } @@ -155,9 +149,7 @@ type relPathFormatter struct { func (f *relPathFormatter) PrintAll(w io.Writer, sep string) error { for _, project := range f.list { - if _, err := fmt.Fprint(w, project.RelPath+sep); err != nil { - return err - } + fmt.Fprint(w, project.RelPath+sep) } return nil } From eaa983d8fd9e46190498b74c36de7228233f498d Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 01:28:44 +0900 Subject: [PATCH 23/30] Cover more --- command/empty_test.go | 10 ++--- command/get_test.go | 42 +++++++++++++++++ command/list_test.go | 2 +- command/setup_test.go | 31 +++++++++++++ command/where.go | 20 ++++++--- command/where_test.go | 90 +++++++++++++++++++++++++++++++++++++ go.mod | 4 -- go.sum | 8 ---- gogh/formatter.go | 16 +++++-- gogh/local.go | 24 +++++++++- internal/context/mock.go | 17 +++++-- internal/testutil/writer.go | 20 +++++++++ 12 files changed, 251 insertions(+), 33 deletions(-) create mode 100644 command/get_test.go create mode 100644 command/setup_test.go create mode 100644 command/where_test.go create mode 100644 internal/testutil/writer.go diff --git a/command/empty_test.go b/command/empty_test.go index bcf5e771..0571cdbf 100644 --- a/command/empty_test.go +++ b/command/empty_test.go @@ -1,17 +1,17 @@ package command import ( - "bytes" "io/ioutil" "net/url" "os" + "path/filepath" "strings" "testing" - "github.com/alecthomas/assert" "github.com/kyoh86/gogh/config" "github.com/kyoh86/gogh/gogh" "github.com/kyoh86/gogh/internal/context" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -20,12 +20,10 @@ func TestEmpty(t *testing.T) { defaultHubClient = &mockHubClient{} tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) + defer os.RemoveAll(tmp) ctx := &context.MockContext{ MRoot: []string{tmp}, MGitHubHost: "github.com", - MStdin: &bytes.Buffer{}, - MStdout: ioutil.Discard, - MStderr: ioutil.Discard, } assert.NoError(t, Pipe(ctx, false, false, false, "echo", []string{"kyoh86/gogh"})) @@ -51,6 +49,8 @@ func TestEmpty(t *testing.T) { // assert.NoError(t, Repos(ctx, "", true, false, false, "", "", "")) assert.NoError(t, Setup(ctx, "gogogh", "zsh")) assert.NoError(t, List(ctx, gogh.ProjectListFormatShort, false, "")) + proj1 := filepath.Join(tmp, "github.com", "kyoh86", "gogh", ".git") + require.NoError(t, os.MkdirAll(proj1, 0755)) assert.NoError(t, Where(ctx, false, false, "gogh")) assert.NoError(t, Root(ctx, false)) } diff --git a/command/get_test.go b/command/get_test.go new file mode 100644 index 00000000..f5c8fe05 --- /dev/null +++ b/command/get_test.go @@ -0,0 +1,42 @@ +package command + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/kyoh86/gogh/gogh" + "github.com/kyoh86/gogh/internal/context" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGet(t *testing.T) { + defaultGitClient = &mockGitClient{} + defaultHubClient = &mockHubClient{} + tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") + require.NoError(t, err) + defer os.RemoveAll(tmp) + ctx := &context.MockContext{ + MRoot: []string{tmp}, + MGitHubHost: "github.com", + } + mustRepo := func(name string) *gogh.Repo { + t.Helper() + repo, err := gogh.ParseRepo(name) + require.NoError(t, err) + return repo + } + assert.NoError(t, GetAll(ctx, false, false, false, gogh.Repos{ + *mustRepo("kyoh86/gogh"), + *mustRepo("kyoh86/vim-gogh"), + })) + assert.EqualError(t, GetAll(ctx, false, false, false, gogh.Repos{ + *mustRepo("https://example.com/kyoh86/gogh"), + }), `not supported host: "example.com"`) + assert.NoError(t, Get(ctx, false, false, false, mustRepo("kyoh86/gogh")), "success getting one") + require.NoError(t, os.MkdirAll(filepath.Join(tmp, "github.com", "kyoh86", "gogh", ".git"), 0755)) + assert.NoError(t, Get(ctx, false, false, false, mustRepo("kyoh86/gogh")), "success getting one that is already exist") + assert.NoError(t, Get(ctx, true, false, false, mustRepo("kyoh86/gogh")), "success updating one that is already exist") +} diff --git a/command/list_test.go b/command/list_test.go index 7617d5bd..e6888e83 100644 --- a/command/list_test.go +++ b/command/list_test.go @@ -6,10 +6,10 @@ import ( "path/filepath" "testing" - "github.com/alecthomas/assert" "github.com/kyoh86/gogh/command" "github.com/kyoh86/gogh/config" "github.com/kyoh86/gogh/gogh" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/command/setup_test.go b/command/setup_test.go new file mode 100644 index 00000000..10a1c5e7 --- /dev/null +++ b/command/setup_test.go @@ -0,0 +1,31 @@ +package command_test + +import ( + "testing" + + "github.com/kyoh86/gogh/command" + "github.com/kyoh86/gogh/config" + "github.com/stretchr/testify/assert" +) + +func ExampleSetupForZsh() { + if err := command.Setup(&config.Config{}, "gogh-cd", "zsh"); err != nil { + panic(err) + } + // Output: + // function gogh-cd { cd $(gogh find $@) } + // eval "$(gogh --completion-script-zsh)" +} + +func ExampleSetupForBash() { + if err := command.Setup(&config.Config{}, "gogh-cd", "bash"); err != nil { + panic(err) + } + // Output: + // function gogh-cd { cd $(gogh find $@) } + // eval "$(gogh --completion-script-bash)" +} + +func TestSetup(t *testing.T) { + assert.EqualError(t, command.Setup(&config.Config{}, "gogh-cd", "invalid"), "unsupported shell \"invalid\"") +} diff --git a/command/where.go b/command/where.go index fce427da..5cd925c8 100644 --- a/command/where.go +++ b/command/where.go @@ -11,9 +11,11 @@ import ( func Where(ctx gogh.Context, primary bool, exact bool, query string) error { log.Printf("info: Finding a repository by query %s", query) - var walk gogh.Walker = gogh.Walk + walk := gogh.Walk + finder := gogh.FindProject if primary { walk = gogh.WalkInPrimary + finder = gogh.FindProjectInPrimary } formatter := gogh.FullPathFormatter() @@ -23,7 +25,7 @@ func Where(ctx gogh.Context, primary bool, exact bool, query string) error { if err != nil { return err } - project, err := gogh.FindProject(ctx, repo) + project, err := finder(ctx, repo) if err != nil { return err } @@ -37,16 +39,20 @@ func Where(ctx gogh.Context, primary bool, exact bool, query string) error { } } - if formatter.Len() > 1 { + switch l := formatter.Len(); { + case l == 1: + if err := formatter.PrintAll(ctx.Stdout(), "\n"); err != nil { + return err + } + case l < 1: + log.Println("error: No repository is found") + return gogh.ProjectNotFound + default: log.Println("error: Multiple repositories are found") if err := formatter.PrintAll(ctx.Stderr(), "\n"); err != nil { return err } return errors.New("try more precise name") - } else { - if err := formatter.PrintAll(ctx.Stdout(), "\n"); err != nil { - return err - } } return nil } diff --git a/command/where_test.go b/command/where_test.go new file mode 100644 index 00000000..45121019 --- /dev/null +++ b/command/where_test.go @@ -0,0 +1,90 @@ +package command + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/kyoh86/gogh/internal/context" + "github.com/kyoh86/gogh/internal/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWhere(t *testing.T) { + root1, err := ioutil.TempDir(os.TempDir(), "gogh-test1") + require.NoError(t, err) + defer os.RemoveAll(root1) + root2, err := ioutil.TempDir(os.TempDir(), "gogh-test2") + require.NoError(t, err) + defer os.RemoveAll(root2) + + proj1 := filepath.Join(root1, "github.com", "kyoh86", "vim-gogh", ".git") + require.NoError(t, os.MkdirAll(proj1, 0755)) + proj2 := filepath.Join(root2, "github.com", "kyoh86", "gogh", ".git") + require.NoError(t, os.MkdirAll(proj2, 0755)) + + assert.EqualError(t, Where(&context.MockContext{ + MRoot: []string{root1, root2}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, false, false, "gogh"), "try more precise name") + + assert.EqualError(t, Where(&context.MockContext{ + MStderr: testutil.DefaultErrorWriter, + MRoot: []string{root1, root2}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, false, false, "gogh"), "error writer") + + assert.EqualError(t, Where(&context.MockContext{ + MRoot: []string{root1, root2}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, true, true, "gogh"), "project not found") + + assert.EqualError(t, Where(&context.MockContext{ + MRoot: []string{root1, root2}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, false, false, "noone"), "project not found") + + assert.NoError(t, Where(&context.MockContext{ + MRoot: []string{root1, root2}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, true, false, "gogh")) + + assert.NoError(t, Where(&context.MockContext{ + MRoot: []string{root1, root2}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, false, true, "gogh")) + + assert.NoError(t, Where(&context.MockContext{ + MRoot: []string{root1, root2}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, true, true, "vim-gogh")) + + assert.EqualError(t, Where(&context.MockContext{ + MStdout: testutil.DefaultErrorWriter, + MRoot: []string{root1, root2}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, true, true, "vim-gogh"), "error writer") + + assert.EqualError(t, Where(&context.MockContext{ + MRoot: []string{root1, root2}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, false, true, ".."), "'.' or '..' is reserved name") + + assert.EqualError(t, Where(&context.MockContext{ + MRoot: []string{"/\x00"}, + MGitHubHost: "github.com", + MGitHubUser: "kyoh86", + }, false, false, "gogh"), "stat /\x00: invalid argument") + +} diff --git a/go.mod b/go.mod index cad34a73..7932e4bd 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,7 @@ module github.com/kyoh86/gogh require ( github.com/BurntSushi/toml v0.3.1 // indirect - github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 - github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 // indirect github.com/alecthomas/kingpin v0.0.0-20190313214811-7613e5d4efd9 - github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect github.com/atotto/clipboard v0.1.1 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c @@ -20,7 +17,6 @@ require ( github.com/mattn/go-isatty v0.0.4 // indirect github.com/mitchellh/go-homedir v1.0.0 // indirect github.com/pkg/errors v0.8.0 - github.com/sergi/go-diff v1.0.0 // indirect github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 github.com/thoas/go-funk v0.0.0-20190328084834-b6996520715f golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect diff --git a/go.sum b/go.sum index e0e8e240..58d9df60 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,7 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kingpin v0.0.0-20190313214811-7613e5d4efd9 h1:ThYLvDFpawmpvAijeMff6W8LoQT/v0ldT3d/qWuPVAk= github.com/alecthomas/kingpin v0.0.0-20190313214811-7613e5d4efd9/go.mod h1:idxgS9pV6OOpAhZvx+gcoGRMX9/tt0iqkw/pNxI0C14= -github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 h1:GDQdwm/gAcJcLAKQQZGOJ4knlw+7rfEQQcmwTbt4p5E= -github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= @@ -54,8 +48,6 @@ github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/gogh/formatter.go b/gogh/formatter.go index 0d485746..dcd08249 100644 --- a/gogh/formatter.go +++ b/gogh/formatter.go @@ -94,7 +94,9 @@ func (f *shortListFormatter) Len() int { func (f *shortListFormatter) PrintAll(w io.Writer, sep string) error { for _, project := range f.list { - fmt.Fprint(w, f.shortName(project)+sep) + if _, err := fmt.Fprint(w, f.shortName(project)+sep); err != nil { + return err + } } return nil } @@ -127,7 +129,9 @@ type fullPathFormatter struct { func (f *fullPathFormatter) PrintAll(w io.Writer, sep string) error { for _, project := range f.list { - fmt.Fprint(w, project.FullPath+sep) + if _, err := fmt.Fprint(w, project.FullPath+sep); err != nil { + return err + } } return nil } @@ -138,7 +142,9 @@ type urlFormatter struct { func (f *urlFormatter) PrintAll(w io.Writer, sep string) error { for _, project := range f.list { - fmt.Fprint(w, "https://"+project.RelPath+sep) + if _, err := fmt.Fprint(w, "https://"+project.RelPath+sep); err != nil { + return err + } } return nil } @@ -149,7 +155,9 @@ type relPathFormatter struct { func (f *relPathFormatter) PrintAll(w io.Writer, sep string) error { for _, project := range f.list { - fmt.Fprint(w, project.RelPath+sep) + if _, err := fmt.Fprint(w, project.RelPath+sep); err != nil { + return err + } } return nil } diff --git a/gogh/local.go b/gogh/local.go index 9ccb7573..d27234c0 100644 --- a/gogh/local.go +++ b/gogh/local.go @@ -27,6 +27,15 @@ var ( // FindProject will find a project (local repository) that matches exactly. func FindProject(ctx Context, repo *Repo) (*Project, error) { + return findProject(ctx, repo, Walk) +} + +// FindProjectInPrimary will find a project (local repository) that matches exactly. +func FindProjectInPrimary(ctx Context, repo *Repo) (*Project, error) { + return findProject(ctx, repo, WalkInPrimary) +} + +func findProject(ctx Context, repo *Repo, walker Walker) (*Project, error) { if err := CheckRepoHost(ctx, repo); err != nil { return nil, err } @@ -34,7 +43,7 @@ func FindProject(ctx Context, repo *Repo) (*Project, error) { var project *Project // Find existing repository first - if err := Walk(ctx, func(p *Project) error { + if err := walker(ctx, func(p *Project) error { if p.RelPath == relPath { project = p return filepath.SkipDir @@ -64,6 +73,19 @@ func FindOrNewProject(ctx Context, repo *Repo) (*Project, error) { } } +// FindOrNewProjectInPrimary will find a project (local repository) that matches exactly or create new one. +func FindOrNewProjectInPrimary(ctx Context, repo *Repo) (*Project, error) { + switch p, err := FindProjectInPrimary(ctx, repo); err { + case ProjectNotFound: + // No repository found, returning new one + return NewProject(ctx, repo) + case nil: + return p, nil + default: + return nil, err + } +} + // NewProject creates a project (local repository) func NewProject(ctx Context, repo *Repo) (*Project, error) { if err := CheckRepoHost(ctx, repo); err != nil { diff --git a/internal/context/mock.go b/internal/context/mock.go index 3119735c..ed5f9867 100644 --- a/internal/context/mock.go +++ b/internal/context/mock.go @@ -1,8 +1,10 @@ package context import ( + "bytes" "context" "io" + "io/ioutil" ) type MockContext struct { @@ -25,15 +27,24 @@ type MockContext struct { } func (c *MockContext) Stdin() io.Reader { - return c.MStdin + if r := c.MStdin; r != nil { + return r + } + return &bytes.Buffer{} } func (c *MockContext) Stdout() io.Writer { - return c.MStdout + if w := c.MStdout; w != nil { + return w + } + return ioutil.Discard } func (c *MockContext) Stderr() io.Writer { - return c.MStderr + if w := c.MStderr; w != nil { + return w + } + return ioutil.Discard } func (c *MockContext) GitHubUser() string { diff --git a/internal/testutil/writer.go b/internal/testutil/writer.go new file mode 100644 index 00000000..c352a174 --- /dev/null +++ b/internal/testutil/writer.go @@ -0,0 +1,20 @@ +package testutil + +import ( + "errors" + "io" +) + +type ErrorWriter struct { + Error error +} + +var ( + DefaultErrorWriter io.Writer = &ErrorWriter{ + Error: errors.New("error writer"), + } +) + +func (w *ErrorWriter) Write(b []byte) (int, error) { + return 0, w.Error +} From 32014bf2efe14ac4663c149e8d0349ff71c9dec6 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 01:30:23 +0900 Subject: [PATCH 24/30] Merge ExampleSetup --- command/setup_test.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/command/setup_test.go b/command/setup_test.go index 10a1c5e7..b24d4dfb 100644 --- a/command/setup_test.go +++ b/command/setup_test.go @@ -8,21 +8,17 @@ import ( "github.com/stretchr/testify/assert" ) -func ExampleSetupForZsh() { +func ExampleSetup() { if err := command.Setup(&config.Config{}, "gogh-cd", "zsh"); err != nil { panic(err) } - // Output: - // function gogh-cd { cd $(gogh find $@) } - // eval "$(gogh --completion-script-zsh)" -} - -func ExampleSetupForBash() { if err := command.Setup(&config.Config{}, "gogh-cd", "bash"); err != nil { panic(err) } // Output: // function gogh-cd { cd $(gogh find $@) } + // eval "$(gogh --completion-script-zsh)" + // function gogh-cd { cd $(gogh find $@) } // eval "$(gogh --completion-script-bash)" } From 185cbb0dd0ae180a81b3cbc970f1a8d77a2e7d64 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 03:56:32 +0900 Subject: [PATCH 25/30] Use testutil --- gogh/formatter_test.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/gogh/formatter_test.go b/gogh/formatter_test.go index 8b26ee81..f711b620 100644 --- a/gogh/formatter_test.go +++ b/gogh/formatter_test.go @@ -2,11 +2,11 @@ package gogh import ( "bytes" - "errors" "io/ioutil" "testing" "github.com/kyoh86/gogh/internal/context" + "github.com/kyoh86/gogh/internal/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -43,7 +43,7 @@ func TestFormatter(t *testing.T) { formatter, err := ProjectListFormatRelPath.Formatter() require.NoError(t, err) formatter.Add(project) - require.EqualError(t, formatter.PrintAll(&invalidWriter{}, ""), "invalid writer") + require.EqualError(t, formatter.PrintAll(testutil.DefaultErrorWriter, ""), "error writer") }) t.Run("full path formatter", func(t *testing.T) { @@ -63,7 +63,7 @@ func TestFormatter(t *testing.T) { formatter, err := ProjectListFormatFullPath.Formatter() require.NoError(t, err) formatter.Add(project) - require.EqualError(t, formatter.PrintAll(&invalidWriter{}, ""), "invalid writer") + require.EqualError(t, formatter.PrintAll(testutil.DefaultErrorWriter, ""), "error writer") }) t.Run("url formatter", func(t *testing.T) { @@ -86,7 +86,7 @@ func TestFormatter(t *testing.T) { formatter, err := ProjectListFormatURL.Formatter() require.NoError(t, err) formatter.Add(project) - require.EqualError(t, formatter.PrintAll(&invalidWriter{}, ""), "invalid writer") + require.EqualError(t, formatter.PrintAll(testutil.DefaultErrorWriter, ""), "error writer") }) t.Run("short formatter", func(t *testing.T) { @@ -121,7 +121,7 @@ func TestFormatter(t *testing.T) { formatter, err := ProjectListFormatShort.Formatter() require.NoError(t, err) formatter.Add(project) - require.EqualError(t, formatter.PrintAll(&invalidWriter{}, ""), "invalid writer") + require.EqualError(t, formatter.PrintAll(testutil.DefaultErrorWriter, ""), "error writer") }) t.Run("invalid formatter", func(t *testing.T) { @@ -130,10 +130,3 @@ func TestFormatter(t *testing.T) { }) } - -type invalidWriter struct { -} - -func (w *invalidWriter) Write([]byte) (int, error) { - return 0, errors.New("invalid writer") -} From 46347f99b807d4a6db7e18738610ffae9c13c4f9 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 03:57:24 +0900 Subject: [PATCH 26/30] Cover more --- gogh/local.go | 16 ++++++---------- gogh/local_test.go | 35 +++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/gogh/local.go b/gogh/local.go index d27234c0..e21ceee6 100644 --- a/gogh/local.go +++ b/gogh/local.go @@ -62,20 +62,16 @@ func findProject(ctx Context, repo *Repo, walker Walker) (*Project, error) { // FindOrNewProject will find a project (local repository) that matches exactly or create new one. func FindOrNewProject(ctx Context, repo *Repo) (*Project, error) { - switch p, err := FindProject(ctx, repo); err { - case ProjectNotFound: - // No repository found, returning new one - return NewProject(ctx, repo) - case nil: - return p, nil - default: - return nil, err - } + return findOrNewProject(ctx, repo, Walk) } // FindOrNewProjectInPrimary will find a project (local repository) that matches exactly or create new one. func FindOrNewProjectInPrimary(ctx Context, repo *Repo) (*Project, error) { - switch p, err := FindProjectInPrimary(ctx, repo); err { + return findOrNewProject(ctx, repo, WalkInPrimary) +} + +func findOrNewProject(ctx Context, repo *Repo, walker Walker) (*Project, error) { + switch p, err := findProject(ctx, repo, walker); err { case ProjectNotFound: // No repository found, returning new one return NewProject(ctx, repo) diff --git a/gogh/local_test.go b/gogh/local_test.go index a0bf173d..31dbb6a1 100644 --- a/gogh/local_test.go +++ b/gogh/local_test.go @@ -72,22 +72,45 @@ func TestParseProject(t *testing.T) { } func TestFindOrNewProject(t *testing.T) { - tmp, err := ioutil.TempDir(os.TempDir(), "gogh-test") + tmp1, err := ioutil.TempDir(os.TempDir(), "gogh-test") + require.NoError(t, err) + defer os.RemoveAll(tmp1) + tmp2, err := ioutil.TempDir(os.TempDir(), "gogh-test") require.NoError(t, err) - ctx := context.MockContext{MRoot: []string{tmp}, MGitHubUser: "kyoh86", MGitHubHost: "github.com"} + defer os.RemoveAll(tmp2) + ctx := context.MockContext{MRoot: []string{tmp1, tmp2}, MGitHubUser: "kyoh86", MGitHubHost: "github.com"} - path := filepath.Join(tmp, "github.com", "kyoh86", "gogh") + path := filepath.Join(tmp1, "github.com", "kyoh86", "gogh") t.Run("not existing repository", func(t *testing.T) { p, err := FindOrNewProject(&ctx, parseURL(t, "ssh://git@github.com/kyoh86/gogh.git")) require.NoError(t, err) assert.Equal(t, path, p.FullPath) + assert.False(t, p.Exists) + assert.Equal(t, []string{"gogh", "kyoh86/gogh", "github.com/kyoh86/gogh"}, p.Subpaths()) + }) + t.Run("not existing repository (in primary)", func(t *testing.T) { + // Create same name repository in other root + inOther := filepath.Join(tmp2, "github.com", "kyoh86", "gogh", ".git") + require.NoError(t, os.MkdirAll(inOther, 0755)) + defer os.RemoveAll(inOther) + p, err := FindOrNewProjectInPrimary(&ctx, parseURL(t, "ssh://git@github.com/kyoh86/gogh.git")) + require.NoError(t, err) + assert.Equal(t, path, p.FullPath) + assert.False(t, p.Exists) assert.Equal(t, []string{"gogh", "kyoh86/gogh", "github.com/kyoh86/gogh"}, p.Subpaths()) }) t.Run("not existing repository with FindProject", func(t *testing.T) { _, err := FindProject(&ctx, parseURL(t, "ssh://git@github.com/kyoh86/gogh.git")) assert.EqualError(t, err, "project not found") }) + t.Run("not existing repository with FindProjectInPrimary", func(t *testing.T) { + inOther := filepath.Join(tmp2, "github.com", "kyoh86", "gogh", ".git") + require.NoError(t, os.MkdirAll(inOther, 0755)) + defer os.RemoveAll(inOther) + _, err := FindProjectInPrimary(&ctx, parseURL(t, "ssh://git@github.com/kyoh86/gogh.git")) + assert.EqualError(t, err, "project not found") + }) t.Run("not supported host URL by FindProject", func(t *testing.T) { _, err := FindOrNewProject(&ctx, parseURL(t, "ssh://git@example.com/kyoh86/gogh.git")) assert.EqualError(t, err, `not supported host: "example.com"`) @@ -107,9 +130,9 @@ func TestFindOrNewProject(t *testing.T) { }) t.Run("existing repository", func(t *testing.T) { // Create same name repository - require.NoError(t, os.MkdirAll(filepath.Join(tmp, "github.com", "kyoh85", "gogh", ".git"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(tmp1, "github.com", "kyoh85", "gogh", ".git"), 0755)) // Create different name repository - require.NoError(t, os.MkdirAll(filepath.Join(tmp, "github.com", "kyoh86", "foo", ".git"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(tmp1, "github.com", "kyoh86", "foo", ".git"), 0755)) // Create target repository require.NoError(t, os.MkdirAll(filepath.Join(path, ".git"), 0755)) defer func() { @@ -132,7 +155,7 @@ func TestFindOrNewProject(t *testing.T) { t.Run("shortest pricese name (name only)", func(t *testing.T) { p, err := FindOrNewProject(&ctx, parseURL(t, "foo")) require.NoError(t, err) - assert.Equal(t, filepath.Join(tmp, "github.com", "kyoh86", "foo"), p.FullPath) + assert.Equal(t, filepath.Join(tmp1, "github.com", "kyoh86", "foo"), p.FullPath) assert.Equal(t, []string{"foo", "kyoh86/foo", "github.com/kyoh86/foo"}, p.Subpaths()) }) }) From 8c613999e8f698eebdc9aadd464cd6a6f11c0500 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 04:02:56 +0900 Subject: [PATCH 27/30] Cover more --- command/root_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 command/root_test.go diff --git a/command/root_test.go b/command/root_test.go new file mode 100644 index 00000000..3af67da9 --- /dev/null +++ b/command/root_test.go @@ -0,0 +1,27 @@ +package command_test + +import ( + "fmt" + + "github.com/kyoh86/gogh/command" + "github.com/kyoh86/gogh/config" +) + +func ExampleRoot() { + ctx := &config.Config{ + VRoot: config.PathListOption{"/foo", "/bar"}, + } + + if err := command.Root(ctx, false); err != nil { + panic(err) + } + fmt.Println() + if err := command.Root(ctx, true); err != nil { + panic(err) + } + // Output: + // /foo + // + // /foo + // /bar +} From 29e7875a3b018617e3dfb75f01838dbaa663ebd7 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 10:16:05 +0900 Subject: [PATCH 28/30] Cover more --- command/empty_test.go | 10 -------- command/git.go | 10 +++++--- command/new_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 command/new_test.go diff --git a/command/empty_test.go b/command/empty_test.go index 0571cdbf..49df3399 100644 --- a/command/empty_test.go +++ b/command/empty_test.go @@ -2,13 +2,11 @@ package command import ( "io/ioutil" - "net/url" "os" "path/filepath" "strings" "testing" - "github.com/kyoh86/gogh/config" "github.com/kyoh86/gogh/gogh" "github.com/kyoh86/gogh/internal/context" "github.com/stretchr/testify/assert" @@ -40,17 +38,9 @@ func TestEmpty(t *testing.T) { *mustRepo("kyoh86/vim-gogh"), })) assert.NoError(t, Get(ctx, false, false, false, mustRepo("kyoh86/gogh"))) - assert.NoError(t, ConfigGetAll(&config.Config{})) - assert.NoError(t, ConfigGet(&config.Config{}, "root")) - assert.NoError(t, ConfigPut(&config.Config{}, "root", "/tmp")) - assert.NoError(t, ConfigUnset(&config.Config{}, "root")) assert.NoError(t, Fork(ctx, false, false, false, false, "", "", mustRepo("kyoh86/gogh"))) - assert.NoError(t, New(ctx, false, "", &url.URL{}, false, false, false, "", "", gogh.ProjectShared("false"), mustRepo("kyoh86/gogh"))) - // assert.NoError(t, Repos(ctx, "", true, false, false, "", "", "")) - assert.NoError(t, Setup(ctx, "gogogh", "zsh")) assert.NoError(t, List(ctx, gogh.ProjectListFormatShort, false, "")) proj1 := filepath.Join(tmp, "github.com", "kyoh86", "gogh", ".git") require.NoError(t, os.MkdirAll(proj1, 0755)) - assert.NoError(t, Where(ctx, false, false, "gogh")) assert.NoError(t, Root(ctx, false)) } diff --git a/command/git.go b/command/git.go index 406d2dce..cb9f5fa4 100644 --- a/command/git.go +++ b/command/git.go @@ -2,6 +2,8 @@ package command import ( "net/url" + "os" + "path/filepath" "github.com/kyoh86/gogh/command/internal" "github.com/kyoh86/gogh/gogh" @@ -17,14 +19,16 @@ type mockGitClient struct { } func (i *mockGitClient) Init(ctx gogh.Context, project *gogh.Project, bare bool, template, separateGitDir string, shared gogh.ProjectShared) error { - return nil + return os.MkdirAll(filepath.Join(project.FullPath, ".git"), 0755) } func (i *mockGitClient) Clone(ctx gogh.Context, project *gogh.Project, remote *url.URL, shallow bool) error { - return nil + return os.MkdirAll(filepath.Join(project.FullPath, ".git"), 0755) } -func (i *mockGitClient) Update(ctx gogh.Context, project *gogh.Project) error { return nil } +func (i *mockGitClient) Update(ctx gogh.Context, project *gogh.Project) error { + return nil +} var defaultGitClient gitClient = &internal.GitClient{} diff --git a/command/new_test.go b/command/new_test.go new file mode 100644 index 00000000..0138117e --- /dev/null +++ b/command/new_test.go @@ -0,0 +1,60 @@ +package command + +import ( + "io/ioutil" + "net/url" + "os" + "testing" + + "github.com/kyoh86/gogh/gogh" + "github.com/kyoh86/gogh/internal/context" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + defaultGitClient = &mockGitClient{} + defaultHubClient = &mockHubClient{} + root, err := ioutil.TempDir(os.TempDir(), "gogh-test") + require.NoError(t, err) + defer os.RemoveAll(root) + + ctx := &context.MockContext{ + MRoot: []string{root}, + MGitHubHost: "github.com", + } + + mustRepo := func(name string) *gogh.Repo { + t.Helper() + repo, err := gogh.ParseRepo(name) + require.NoError(t, err) + return repo + } + assert.NoError(t, New( + ctx, + false, + "", + &url.URL{}, + false, + false, + false, + "", + "", + gogh.ProjectShared("false"), + mustRepo("kyoh86/gogh"), + )) + + assert.EqualError(t, New( + ctx, + false, + "", + &url.URL{}, + false, + false, + false, + "", + "", + gogh.ProjectShared("false"), + mustRepo("kyoh86/gogh"), + ), "project already exists") +} From e2dd0b6e3e002d7b26951a8fabf377c5e8750c02 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 10:55:27 +0900 Subject: [PATCH 29/30] Tidy go modules --- go.mod | 10 ++++------ go.sum | 30 +++++++++++++----------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 7932e4bd..4187f009 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/kyoh86/gogh require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/alecthomas/kingpin v0.0.0-20190313214811-7613e5d4efd9 - github.com/atotto/clipboard v0.1.1 // indirect + github.com/atotto/clipboard v0.1.2 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c github.com/github/hub v2.10.0+incompatible @@ -13,15 +13,13 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/kyoh86/xdg v1.0.0 - github.com/mattn/go-colorable v0.0.9 // indirect - github.com/mattn/go-isatty v0.0.4 // indirect - github.com/mitchellh/go-homedir v1.0.0 // indirect + github.com/mattn/go-colorable v0.1.1 // indirect + github.com/mattn/go-isatty v0.0.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/pkg/errors v0.8.0 github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 github.com/thoas/go-funk v0.0.0-20190328084834-b6996520715f - golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.1 ) diff --git a/go.sum b/go.sum index 58d9df60..5770dc66 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5Vpd github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/atotto/clipboard v0.1.1 h1:WSoEbAS70E5gw8FbiqFlp69MGsB6dUb4l+0AGGLiVGw= -github.com/atotto/clipboard v0.1.1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= +github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/comail/colog v0.0.0-20160416085026-fba8e7b1f46c h1:bzYQ6WpR+t35/y19HUkolcg7SYeWZ15IclC9Z4naGHI= @@ -38,12 +38,13 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kyoh86/xdg v1.0.0 h1:TD1layQ0epNApNwGRblnQnT3S/2UH/gCQN1cmXWotvE= github.com/kyoh86/xdg v1.0.0/go.mod h1:Z5mDqe0fxyxn3W2yTxsBAOQqIrXADQIh02wrTnaRM38= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -55,25 +56,20 @@ github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 h1:Ko2LQMrRU+Oy github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/thoas/go-funk v0.0.0-20190328084834-b6996520715f h1:hucXnlEXfHvrY7vI9hP1bJX/olUUt5a6u3QvxXrtANw= github.com/thoas/go-funk v0.0.0-20190328084834-b6996520715f/go.mod h1:mlR+dHGb+4YgXkf13rkQTuzrneeHANxOm6+ZnEV9HsA= +golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac h1:7d7lG9fHOLdL6jZPtnV4LpI41SbohIJ1Atq7U991dMg= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87 h1:GqwDwfvIpC33dK9bA1fD+JiDUNsuAiQiEkpHqUKze4o= golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 7cfcb9a2434d9810260f2e1b27e31a3e82636cd8 Mon Sep 17 00:00:00 2001 From: Kyoichiro Yamada Date: Sun, 31 Mar 2019 11:05:20 +0900 Subject: [PATCH 30/30] Update README for v1 --- README.md | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6e27a046..bcdf4aab 100644 --- a/README.md +++ b/README.md @@ -57,19 +57,19 @@ brew install gogh ## CONFIGURATIONS -It's possible to change targets by a preference **TOML file**. +It's possible to change targets by a preference **YAML file**. If you don't set `--config` flag or `GOGH_CONFIG` environment variable, -`gogh` loads configurations from `${XDG_CONFIG_HOME:-$HOME/.config}/gogh/config.toml` +`gogh` loads configurations from `${XDG_CONFIG_HOME:-$HOME/.config}/gogh/config.yaml` Each of propoerties are able to be overwritten by environment variables. -### (REQUIRED) GitHubUser +### (REQUIRED) `github.user` A name of your GitHub user (i.e. `kyoh86`). If an environment variable `GOGH_GITHUB_USER` is set, its value is used instead. -### Root +### `root` The paths to directory under which cloned repositories are placed. See [DIRECTORY STRUCTURES](#DIRECTORY+STRUCTURES) below. Default: `~/go/src`. @@ -80,24 +80,31 @@ You may want to specify `$GOPATH/src` as a secondary root. If an environment variable `GOGH_ROOT` is set, its value is used instead. -### LogLevel +### `log.level` The level to output logs (debug, info, warn, error or panic). Default: `warn`. If an environment variable `GOGH_LOG_LEVEL` is set, its value is used instead. -### GitHubToken +### `github.token` The token to connect GitHub API. If an environment variable `GOGH_GITHUB_TOKEN` is set, its value is used instead. -### GitHubHost +### `github.host` The host name to connect to GitHub. Default: `github.com`. If an environment variable `GOGH_GITHUB_HOST` is set, its value is used instead. +### `log.(date|time|microseconds|longfile|shortfile|utc)` + +It can be customized that what log prints with message. Default: `log.time` is "yes", others are "no". +`"yes"` means that its optional property should be printed. + +If an environment variable `GOGH_LOG_(DATE|TIME|MICROSECONDS|LONGFILE|SHORTFILE|UTC)` is set, its value is used instead. + ## COMMANDS ``` @@ -173,7 +180,23 @@ Show a list of repositories for a user. ### `root` -Prints repositories' root (i.e. `gogh.root`). Without `--all` option, the primary one is shown. +Print repositories' root (i.e. `config get root`). Without `--all` option, the primary one is shown. + +### `config get-all` + +Print all configuration options value. + +### `config get` + +Print one configuration option value. + +### `config put` + +Set or add one configuration option. + +### `config unset` + +Unset one configuration option. ## ENVIRONMENT VARIABLES @@ -182,7 +205,7 @@ Some environment variables are used for flags. ### GOGH_CONFIG You can set it instead of `--config` flag (configuration file path). -Default: `${XDG_CONFIG_HOME:-$HOME/.config}/gogh/config.toml`. +Default: `${XDG_CONFIG_HOME:-$HOME/.config}/gogh/config.yaml`. ### GOGH_FLAG_ROOT_ALL