6
0
mirror of https://github.com/grdl/git-get.git synced 2026-02-04 15:39:46 +00:00

Update help commands and errors messages

This commit is contained in:
Grzegorz Dlugoszewski
2020-06-19 16:39:26 +02:00
parent de51c05158
commit 4868713746
11 changed files with 100 additions and 73 deletions

View File

@@ -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

View File

@@ -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 <repo>",
Short: "git get",
Use: "git get <REPO>",
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 <REPO> doesn't have a specified host.")
cmd.PersistentFlags().StringP(cfg.KeyDump, "d", "", "Path to a dump file listing repos to clone. Ignored when <REPO> 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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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 <REPO> argument or --dump flag")
}
if c.URL != "" {
return cloneSingleRepo(c)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}