From 48687137466e0a67a91900ece10fbba10ab8aced Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Fri, 19 Jun 2020 16:39:26 +0200 Subject: [PATCH] Update help commands and errors messages --- README.md | 4 +++- cmd/get/main.go | 30 ++++++++++++++++++++---------- cmd/list/main.go | 23 ++++++++++++++--------- pkg/cfg/config.go | 15 +++++++++------ pkg/cfg/config_test.go | 25 +++++++++++++------------ pkg/dump.go | 8 ++++---- pkg/get.go | 5 ++++- pkg/list.go | 7 +++---- pkg/repo/repo.go | 26 +++++++++++++++----------- pkg/repo/status.go | 26 +++++++++++++------------- pkg/url.go | 4 ++-- 11 files changed, 100 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 7d98d38..716dfd8 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ `git-get` is a better way to clone, organize and manage multiple git repositories. It gives you two new git commands: -- **`git get`** clones repositories into an organized directory structure (like golang's [`go get`](https://golang.org/cmd/go/)). It's dotfiles friendly - you can clone multiple repositories listed in a file. +- **`git get`** clones repositories into an automatically created directory tree based on repo's URL (like golang's [`go get`](https://golang.org/cmd/go/)). It's dotfiles friendly, you can clone multiple repositories listed in a file. - **`git list`** shows status of all your git repositories and their branches. ![Example](./docs/example.svg) + ## Installation Using Homebrew: @@ -23,6 +24,7 @@ Or grab the [latest release](https://github.com/grdl/git-get/releases) and put t Each release contains two binaries: `git-get` and `git-list`. When put on PATH, git automatically recognizes them as custom commands and allows to call them as `git get` or `git list`. + ## Usage diff --git a/cmd/get/main.go b/cmd/get/main.go index fe0f4d6..2e3d792 100644 --- a/cmd/get/main.go +++ b/cmd/get/main.go @@ -8,25 +8,35 @@ import ( "github.com/spf13/viper" ) +const example = ` git get grdl/git-get + git get https://github.com/grdl/git-get.git + git get git@github.com:grdl/git-get.git + git get -d path/to/dump/file` + var cmd = &cobra.Command{ - Use: "git-get ", - Short: "git get", + Use: "git get ", + Short: "Clone git repository into an automatically created directory tree based on the repo's URL.", + Example: example, RunE: run, Args: cobra.MaximumNArgs(1), // TODO: add custom validator Version: cfg.Version(), - SilenceUsage: true, + SilenceUsage: true, // We don't want to show usage on legit errors (eg, wrong path, repo already existing etc.) } func init() { - cmd.PersistentFlags().StringP(cfg.KeyReposRoot, "r", "", "repos root") - cmd.PersistentFlags().StringP(cfg.KeyPrivateKey, "p", "", "SSH private key path") - cmd.PersistentFlags().StringP(cfg.KeyDump, "d", "", "Dump file path") - cmd.PersistentFlags().StringP(cfg.KeyBranch, "b", cfg.DefBranch, "Branch (or tag) to checkout after cloning") + cmd.PersistentFlags().StringP(cfg.KeyBranch, "b", cfg.DefBranch, "Branch (or tag) to checkout after cloning. Tag name needs to be prefixed with 'refs/tags/'.") + cmd.PersistentFlags().StringP(cfg.KeyDefaultHost, "t", cfg.DefDefaultHost, "Host to use when doesn't have a specified host.") + cmd.PersistentFlags().StringP(cfg.KeyDump, "d", "", "Path to a dump file listing repos to clone. Ignored when argument is used.") + cmd.PersistentFlags().StringP(cfg.KeyPrivateKey, "p", "", "Path to SSH private key. (default \"~/.ssh/id_rsa\")") + cmd.PersistentFlags().StringP(cfg.KeyReposRoot, "r", "", "Path to repos root where repositories are cloned. (default \"~/repositories\")") + cmd.PersistentFlags().BoolP("help", "h", false, "Print this help and exit.") + cmd.PersistentFlags().BoolP("version", "v", false, "Print version and exit.") - viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) - viper.BindPFlag(cfg.KeyPrivateKey, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) - viper.BindPFlag(cfg.KeyDump, cmd.PersistentFlags().Lookup(cfg.KeyDump)) viper.BindPFlag(cfg.KeyBranch, cmd.PersistentFlags().Lookup(cfg.KeyBranch)) + viper.BindPFlag(cfg.KeyDefaultHost, cmd.PersistentFlags().Lookup(cfg.KeyDefaultHost)) + viper.BindPFlag(cfg.KeyDump, cmd.PersistentFlags().Lookup(cfg.KeyDump)) + viper.BindPFlag(cfg.KeyPrivateKey, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) + viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) } func run(cmd *cobra.Command, args []string) error { diff --git a/cmd/list/main.go b/cmd/list/main.go index 82c39c6..157e50b 100644 --- a/cmd/list/main.go +++ b/cmd/list/main.go @@ -1,32 +1,37 @@ package main import ( + "fmt" "git-get/pkg" "git-get/pkg/cfg" + "strings" "github.com/spf13/cobra" "github.com/spf13/viper" ) var cmd = &cobra.Command{ - Use: "git-list", - Short: "git list", + Use: "git list", + Short: "List all repositories cloned by 'git get' and their status.", RunE: run, Args: cobra.NoArgs, Version: cfg.Version(), - SilenceUsage: true, + SilenceUsage: true, // We don't want to show usage on legit errors (eg, wrong path, repo already existing etc.) } func init() { - cmd.PersistentFlags().StringP(cfg.KeyReposRoot, "r", "", "repos root") - cmd.PersistentFlags().StringP(cfg.KeyPrivateKey, "p", "", "SSH private key path") - cmd.PersistentFlags().BoolP(cfg.KeyFetch, "f", false, "Fetch from remotes when listing repositories") - cmd.PersistentFlags().StringP(cfg.KeyOutput, "o", cfg.DefOutput, "output format.") + cmd.PersistentFlags().BoolP(cfg.KeyFetch, "f", false, "First fetch from remotes before listing repositories.") + cmd.PersistentFlags().StringP(cfg.KeyOutput, "o", cfg.DefOutput, fmt.Sprintf("Output format. Allowed values: [%s].", strings.Join(cfg.AllowedOut, ", "))) + cmd.PersistentFlags().StringP(cfg.KeyPrivateKey, "p", "", "Path to SSH private key. (default \"~/.ssh/id_rsa\")") + cmd.PersistentFlags().StringP(cfg.KeyReposRoot, "r", "", "Path to repos root where repositories are cloned. (default \"~/repositories\")") + cmd.PersistentFlags().BoolP("help", "h", false, "Print this help and exit.") + cmd.PersistentFlags().BoolP("version", "v", false, "Print version and exit.") - viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) - viper.BindPFlag(cfg.KeyPrivateKey, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) viper.BindPFlag(cfg.KeyFetch, cmd.PersistentFlags().Lookup(cfg.KeyFetch)) viper.BindPFlag(cfg.KeyOutput, cmd.PersistentFlags().Lookup(cfg.KeyOutput)) + viper.BindPFlag(cfg.KeyPrivateKey, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) + viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) + } func run(cmd *cobra.Command, args []string) error { diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index cd30560..c3587f3 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -11,33 +11,36 @@ import ( "github.com/spf13/viper" ) -// Gitconfig section name and env var prefix +// GitgetPrefix is the name of the gitconfig section name and the env var prefix. const GitgetPrefix = "gitget" -// Flag keys and their default values +// CLI flag keys and their default values. const ( KeyBranch = "branch" DefBranch = "master" KeyDump = "dump" - KeyDefaultHost = "defaultHost" + KeyDefaultHost = "host" DefDefaultHost = "github.com" KeyFetch = "fetch" KeyOutput = "out" DefOutput = OutTree KeyPrivateKey = "privateKey" DefPrivateKey = "id_rsa" - KeyReposRoot = "reposRoot" + KeyReposRoot = "root" DefReposRoot = "repositories" ) -// Allowed values for the --out flag +// Values for the --out flag. const ( OutDump = "dump" OutFlat = "flat" - OutTree = "tree" OutSmart = "smart" + OutTree = "tree" ) +// AllowedOut are allowed values for the --out flag. +var AllowedOut = []string{OutDump, OutFlat, OutSmart, OutTree} + // Version metadata set by ldflags during the build. var ( version string diff --git a/pkg/cfg/config_test.go b/pkg/cfg/config_test.go index 04868ce..6d05cca 100644 --- a/pkg/cfg/config_test.go +++ b/pkg/cfg/config_test.go @@ -1,6 +1,7 @@ package cfg import ( + "fmt" "os" "path" "strings" @@ -10,9 +11,9 @@ import ( "github.com/spf13/viper" ) -const ( - EnvDefaultHost = "GITGET_DEFAULTHOST" - EnvReposRoot = "GITGET_REPOSROOT" +var ( + envDefaultHost = strings.ToUpper(fmt.Sprintf("%s_%s", GitgetPrefix, KeyDefaultHost)) + envReposRoot = strings.ToUpper(fmt.Sprintf("%s_%s", GitgetPrefix, KeyReposRoot)) ) func newConfigWithFullGitconfig() *gitconfig { @@ -64,8 +65,8 @@ func newConfigWithEmptyGitconfig() *gitconfig { } func newConfigWithEnvVars() *gitconfig { - _ = os.Setenv(EnvDefaultHost, "env.host") - _ = os.Setenv(EnvReposRoot, "env.root") + _ = os.Setenv(envDefaultHost, "env.host") + _ = os.Setenv(envReposRoot, "env.root") return &gitconfig{ Config: nil, @@ -79,8 +80,8 @@ func newConfigWithGitconfigAndEnvVars() *gitconfig { gitget.AddOption(KeyReposRoot, "file.root") gitget.AddOption(KeyDefaultHost, "file.host") - _ = os.Setenv(EnvDefaultHost, "env.host") - _ = os.Setenv(EnvReposRoot, "env.root") + _ = os.Setenv(envDefaultHost, "env.host") + _ = os.Setenv(envReposRoot, "env.root") return &gitconfig{ Config: cfg, @@ -92,8 +93,8 @@ func newConfigWithEmptySectionAndEnvVars() *gitconfig { _ = cfg.Raw.Section(GitgetPrefix) - _ = os.Setenv(EnvDefaultHost, "env.host") - _ = os.Setenv(EnvReposRoot, "env.root") + _ = os.Setenv(envDefaultHost, "env.host") + _ = os.Setenv(envReposRoot, "env.root") return &gitconfig{ Config: cfg, @@ -107,7 +108,7 @@ func newConfigWithMixed() *gitconfig { gitget.AddOption(KeyReposRoot, "file.root") gitget.AddOption(KeyDefaultHost, "file.host") - _ = os.Setenv(EnvDefaultHost, "env.host") + _ = os.Setenv(envDefaultHost, "env.host") return &gitconfig{ Config: cfg, @@ -150,9 +151,9 @@ func TestConfig(t *testing.T) { // Unset env variables and reset viper registry after each test viper.Reset() - err := os.Unsetenv(EnvDefaultHost) + err := os.Unsetenv(envDefaultHost) checkFatal(t, err) - err = os.Unsetenv(EnvReposRoot) + err = os.Unsetenv(envReposRoot) checkFatal(t, err) } } diff --git a/pkg/dump.go b/pkg/dump.go index db9820a..0ffd6c3 100644 --- a/pkg/dump.go +++ b/pkg/dump.go @@ -9,8 +9,8 @@ import ( ) var ( - errInvalidNumberOfElements = errors.New("More than two space-separated 2 elements on the line") - errEmptyLine = errors.New("Empty line") + errInvalidNumberOfElements = errors.New("more than two space-separated 2 elements on the line") + errEmptyLine = errors.New("empty line") ) type parsedLine struct { @@ -22,7 +22,7 @@ type parsedLine struct { func parseDumpFile(path string) ([]parsedLine, error) { file, err := os.Open(path) if err != nil { - return nil, errors.Wrapf(err, "Failed opening dump file %s", path) + return nil, errors.Wrapf(err, "failed opening dump file %s", path) } defer file.Close() @@ -34,7 +34,7 @@ func parseDumpFile(path string) ([]parsedLine, error) { line++ parsed, err := parseLine(scanner.Text()) if err != nil && !errors.Is(errEmptyLine, err) { - return nil, errors.Wrapf(err, "Failed parsing line %d", line) + return nil, errors.Wrapf(err, "failed parsing dump file line %d", line) } parsedLines = append(parsedLines, parsed) diff --git a/pkg/get.go b/pkg/get.go index 0c9584e..c1e33a8 100644 --- a/pkg/get.go +++ b/pkg/get.go @@ -1,6 +1,7 @@ package pkg import ( + "fmt" "git-get/pkg/repo" "path" ) @@ -16,7 +17,9 @@ type GetCfg struct { // Get executes the "git get" command. func Get(c *GetCfg) error { - // TODO: show something when no args + if c.URL == "" && c.Dump == "" { + return fmt.Errorf("missing argument or --dump flag") + } if c.URL != "" { return cloneSingleRepo(c) diff --git a/pkg/list.go b/pkg/list.go index 0b1f685..8ab4c7c 100644 --- a/pkg/list.go +++ b/pkg/list.go @@ -51,8 +51,7 @@ func List(c *ListCfg) error { case cfg.OutDump: printer = &print.DumpPrinter{} default: - // TODO: fix - return fmt.Errorf("invalid --out flag; allowed values: %v", []string{cfg.OutFlat, cfg.OutTree, cfg.OutSmart}) + return fmt.Errorf("invalid --out flag; allowed values: [%s]", strings.Join(cfg.AllowedOut, ", ")) } fmt.Println(printer.Print(c.Root, repos)) @@ -63,7 +62,7 @@ func findRepos(root string) ([]string, error) { repos = []string{} if _, err := os.Stat(root); err != nil { - return nil, fmt.Errorf("Repos root %s does not exist or can't be accessed", root) + return nil, fmt.Errorf("repos root %s doesn't exist or can't be accessed", root) } walkOpts := &godirwalk.Options{ @@ -79,7 +78,7 @@ func findRepos(root string) ([]string, error) { } if len(repos) == 0 { - return nil, fmt.Errorf("No git repos found in repos root %s", root) + return nil, fmt.Errorf("no git repos found in root path %s", root) } return repos, nil diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index 7fcdca9..12c5820 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -35,6 +35,7 @@ type CloneOpts struct { IgnoreExisting bool } +// Clone clones repository specified in CloneOpts. func Clone(opts *CloneOpts) (*Repo, error) { var progress io.Writer if !opts.Quiet { @@ -82,25 +83,27 @@ func Clone(opts *CloneOpts) (*Repo, error) { return nil, nil } - return nil, errors.Wrap(err, "Failed cloning repo") + return nil, errors.Wrapf(err, "failed cloning %s", opts.URL.String()) } return New(repo, opts.Path), nil } -func Open(repoPath string) (*Repo, error) { - repo, err := git.PlainOpen(repoPath) +// Open opens a repository on a given path. +func Open(path string) (*Repo, error) { + repo, err := git.PlainOpen(path) if err != nil { - return nil, errors.Wrap(err, "Failed opening repo") + return nil, errors.Wrapf(err, "failed opening repo %s", path) } - return New(repo, repoPath), nil + return New(repo, path), nil } -func New(repo *git.Repository, repoPath string) *Repo { +// New returns a new Repo instance from a given go-git Repository. +func New(repo *git.Repository, path string) *Repo { return &Repo{ Repository: repo, - Path: repoPath, + Path: path, Status: &RepoStatus{}, } } @@ -109,13 +112,13 @@ func New(repo *git.Repository, repoPath string) *Repo { func (r *Repo) Fetch() error { remotes, err := r.Remotes() if err != nil { - return errors.Wrap(err, "Failed getting remotes") + return errors.Wrapf(err, "failed getting remotes of repo %s", r.Path) } for _, remote := range remotes { err = remote.Fetch(&git.FetchOptions{}) if err != nil { - return errors.Wrapf(err, "Failed fetching remote %s", remote.Config().Name) + return errors.Wrapf(err, "failed fetching remote %s", remote.Config().Name) } } @@ -126,12 +129,12 @@ func sshKeyAuth() (transport.AuthMethod, error) { privateKey := viper.GetString(cfg.KeyPrivateKey) sshKey, err := ioutil.ReadFile(privateKey) if err != nil { - return nil, errors.Wrapf(err, "Failed to open ssh private key %s", privateKey) + return nil, errors.Wrapf(err, "failed to open ssh private key %s", privateKey) } signer, err := ssh.ParsePrivateKey([]byte(sshKey)) if err != nil { - return nil, errors.Wrapf(err, "Failed to parse ssh private key %s", privateKey) + return nil, errors.Wrapf(err, "failed to parse ssh private key %s", privateKey) } // TODO: can it ba a different user @@ -139,6 +142,7 @@ func sshKeyAuth() (transport.AuthMethod, error) { return auth, nil } +// CurrentBranchStatus returns the BranchStatus of a currently checked out branch. func (r *Repo) CurrentBranchStatus() *BranchStatus { if r.Status.CurrentBranch == StatusDetached || r.Status.CurrentBranch == StatusUnknown { return nil diff --git a/pkg/repo/status.go b/pkg/repo/status.go index cca89d1..98401ab 100644 --- a/pkg/repo/status.go +++ b/pkg/repo/status.go @@ -48,13 +48,13 @@ func (r *Repo) LoadStatus() error { if viper.GetBool(cfg.KeyFetch) { err := r.Fetch() if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { - return errors.Wrap(err, "Failed fetching from remotes") + return errors.Wrapf(err, "failed running git fetch on a repo %s", r.Path) } } wt, err := r.Worktree() if err != nil { - return errors.Wrap(err, "Failed getting worktree") + return errors.Wrapf(err, "failed getting worktree %s", r.Path) } // worktree.Status doesn't load gitignore patterns that are defined outside of .gitignore file using excludesfile. @@ -62,19 +62,19 @@ func (r *Repo) LoadStatus() error { // TODO: variables are not expanded so if excludesfile is declared like "~/gitignore_global" or "$HOME/gitignore_global", this will fail to open it globalPatterns, err := gitignore.LoadGlobalPatterns(osfs.New("")) if err != nil { - return errors.Wrap(err, "Failed loading global gitignore patterns") + return errors.Wrap(err, "failed loading global gitignore patterns") } wt.Excludes = append(wt.Excludes, globalPatterns...) systemPatterns, err := gitignore.LoadSystemPatterns(osfs.New("")) if err != nil { - return errors.Wrap(err, "Failed loading system gitignore patterns") + return errors.Wrap(err, "failed loading system gitignore patterns") } wt.Excludes = append(wt.Excludes, systemPatterns...) status, err := wt.Status() if err != nil { - return errors.Wrap(err, "Failed getting worktree status") + return errors.Wrapf(err, "failed getting status of worktree %s", r.Path) } r.Status.HasUncommittedChanges = hasUncommitted(status) @@ -133,7 +133,7 @@ func currentBranch(r *Repo) string { func (r *Repo) loadBranchesStatus() error { iter, err := r.Branches() if err != nil { - return errors.Wrap(err, "Failed getting branches iterator") + return errors.Wrapf(err, "failed getting branches iterator for repo %s", r.Path) } err = iter.ForEach(func(reference *plumbing.Reference) error { @@ -146,12 +146,12 @@ func (r *Repo) loadBranchesStatus() error { return nil }) if err != nil { - return errors.Wrap(err, "Failed iterating over branches") + return errors.Wrapf(err, "failed iterating over branches of repo %s", r.Path) } // Sort branches by name (but with "master" always at the top). It's useful to have them sorted for printing and testing. sort.Slice(r.Status.Branches, func(i, j int) bool { - if r.Status.Branches[i].Name == "master" { + if r.Status.Branches[i].Name == cfg.DefBranch { return true } @@ -196,7 +196,7 @@ func (r *Repo) newBranchStatus(branch string) (*BranchStatus, error) { func (r *Repo) upstream(branch string) (string, error) { cfg, err := r.Config() if err != nil { - return "", errors.Wrap(err, "Failed getting repo config") + return "", errors.Wrapf(err, "failed getting config of repo %s", r.Path) } // Check if our branch exists in "branch" config sections. If not, it doesn't have an upstream configured. @@ -220,22 +220,22 @@ func (r *Repo) upstream(branch string) (string, error) { func (r *Repo) aheadBehind(localBranch string, upstreamBranch string) (ahead int, behind int, err error) { localHash, err := r.ResolveRevision(plumbing.Revision(localBranch)) if err != nil { - return 0, 0, errors.Wrapf(err, "Failed resolving revision %s", localBranch) + return 0, 0, errors.Wrapf(err, "failed resolving revision %s", localBranch) } upstreamHash, err := r.ResolveRevision(plumbing.Revision(upstreamBranch)) if err != nil { - return 0, 0, errors.Wrapf(err, "Failed resolving revision %s", upstreamBranch) + return 0, 0, errors.Wrapf(err, "failed resolving revision %s", upstreamBranch) } behind, err = r.revlistCount(*localHash, *upstreamHash) if err != nil { - return 0, 0, errors.Wrapf(err, "Failed counting commits behind %s", upstreamBranch) + return 0, 0, errors.Wrapf(err, "failed counting commits behind %s", upstreamBranch) } ahead, err = r.revlistCount(*upstreamHash, *localHash) if err != nil { - return 0, 0, errors.Wrapf(err, "Failed counting commits ahead of %s", upstreamBranch) + return 0, 0, errors.Wrapf(err, "failed counting commits ahead of %s", upstreamBranch) } return ahead, behind, nil diff --git a/pkg/url.go b/pkg/url.go index afb42fd..68a6bf5 100644 --- a/pkg/url.go +++ b/pkg/url.go @@ -9,7 +9,7 @@ import ( "github.com/pkg/errors" ) -var errEmptyURLPath = errors.New("Parsed URL path is empty") +var errEmptyURLPath = errors.New("parsed URL path is empty") // scpSyntax matches the SCP-like addresses used by the ssh protocol (eg, [user@]host.xz:path/to/repo.git/). // See: https://golang.org/src/cmd/go/internal/get/vcs.go @@ -30,7 +30,7 @@ func ParseURL(rawURL string, defaultHost string) (url *urlpkg.URL, err error) { } else { url, err = urlpkg.Parse(rawURL) if err != nil { - return nil, errors.Wrap(err, "Failed parsing URL") + return nil, errors.Wrapf(err, "failed parsing URL %s", rawURL) } }