diff --git a/README.md b/README.md index fd382da..2a397de 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Flags: -h, --help Print this help and exit. -t, --host Host to use when doesn't have a specified host. (default "github.com") -r, --root Path to repos root where repositories are cloned. (default "~/repositories") + -s, --skip-host Don't create a directory for host. -v, --version Print version and exit. ``` @@ -143,11 +144,14 @@ export GITGET_ROOT=/path/to/my/repos You can define a `[gitget]` section inside your global `.gitconfig` file and set the configuration flags there. A recommended pattern is to set `root` and `host` variables there if you don't want to use the defaults. +If all of your repos come from the same host and you find creating directory for it redundant, you can use the `skip-host` flag to skip creating it. + Here's an example of a working snippet from `.gitconfig` file: ``` [gitget] root = /path/to/my/repos host = gitlab.com + skip-host = true ``` diff --git a/cmd/get/main.go b/cmd/get/main.go index de445bd..97313d2 100644 --- a/cmd/get/main.go +++ b/cmd/get/main.go @@ -28,6 +28,7 @@ func init() { cmd.PersistentFlags().StringP(cfg.KeyBranch, "b", "", "Branch (or tag) to checkout after cloning.") cmd.PersistentFlags().StringP(cfg.KeyDefaultHost, "t", cfg.Defaults[cfg.KeyDefaultHost], "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().BoolP(cfg.KeySkipHost, "s", false, "Don't create a directory for host.") cmd.PersistentFlags().StringP(cfg.KeyReposRoot, "r", cfg.Defaults[cfg.KeyReposRoot], "Path to repos root where repositories are cloned.") cmd.PersistentFlags().BoolP("help", "h", false, "Print this help and exit.") cmd.PersistentFlags().BoolP("version", "v", false, "Print version and exit.") @@ -36,6 +37,7 @@ func init() { viper.BindPFlag(cfg.KeyDefaultHost, cmd.PersistentFlags().Lookup(cfg.KeyDefaultHost)) viper.BindPFlag(cfg.KeyDump, cmd.PersistentFlags().Lookup(cfg.KeyDump)) viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) + viper.BindPFlag(cfg.KeySkipHost, cmd.PersistentFlags().Lookup(cfg.KeySkipHost)) cfg.Init(&git.ConfigGlobal{}) } @@ -49,11 +51,12 @@ func run(cmd *cobra.Command, args []string) error { cfg.Expand(cfg.KeyReposRoot) config := &pkg.GetCfg{ - Branch: viper.GetString(cfg.KeyBranch), - DefHost: viper.GetString(cfg.KeyDefaultHost), - Dump: viper.GetString(cfg.KeyDump), - Root: viper.GetString(cfg.KeyReposRoot), - URL: url, + Branch: viper.GetString(cfg.KeyBranch), + DefHost: viper.GetString(cfg.KeyDefaultHost), + Dump: viper.GetString(cfg.KeyDump), + SkipHost: viper.GetBool(cfg.KeySkipHost), + Root: viper.GetString(cfg.KeyReposRoot), + URL: url, } return pkg.Get(config) } diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index 9381f76..79b9171 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -22,6 +22,7 @@ var ( KeyDefaultHost = "host" KeyFetch = "fetch" KeyOutput = "out" + KeySkipHost = "skip-host" KeyReposRoot = "root" ) @@ -30,6 +31,7 @@ var Defaults = map[string]string{ KeyDefaultHost: "github.com", KeyOutput: OutTree, KeyReposRoot: fmt.Sprintf("~%c%s", filepath.Separator, "repositories"), + // KeySkipHost: "false", } // Values for the --out flag. @@ -78,6 +80,7 @@ func Init(cfg Gitconfig) { func readGitconfig(cfg Gitconfig) { var lines []string + // TODO: Can we somehow iterate over all possible flags? for key := range Defaults { if val := cfg.Get(fmt.Sprintf("%s.%s", GitgetPrefix, key)); val != "" { lines = append(lines, fmt.Sprintf("%s=%s", key, val)) @@ -86,6 +89,11 @@ func readGitconfig(cfg Gitconfig) { viper.SetConfigType("env") viper.ReadConfig(bytes.NewBuffer([]byte(strings.Join(lines, "\n")))) + + // TODO: A hacky way to read boolean flag from gitconfig. Find a cleaner way. + if val := cfg.Get(fmt.Sprintf("%s.%s", GitgetPrefix, KeySkipHost)); strings.ToLower(val) == "true" { + viper.Set(KeySkipHost, true) + } } // Expand applies the variables expansion to a viper config of given key. diff --git a/pkg/get.go b/pkg/get.go index 37e57aa..f4ab85d 100644 --- a/pkg/get.go +++ b/pkg/get.go @@ -9,11 +9,12 @@ import ( // GetCfg provides configuration for the Get command. type GetCfg struct { - Branch string - DefHost string - Dump string - Root string - URL string + Branch string + DefHost string + Dump string + Root string + SkipHost bool + URL string } // Get executes the "git get" command. @@ -40,7 +41,7 @@ func cloneSingleRepo(c *GetCfg) error { opts := &git.CloneOpts{ URL: url, - Path: filepath.Join(c.Root, URLToPath(url)), + Path: filepath.Join(c.Root, URLToPath(*url, c.SkipHost)), Branch: c.Branch, } @@ -63,7 +64,7 @@ func cloneDumpFile(c *GetCfg) error { opts := &git.CloneOpts{ URL: url, - Path: filepath.Join(c.Root, URLToPath(url)), + Path: filepath.Join(c.Root, URLToPath(*url, c.SkipHost)), Branch: line.branch, } diff --git a/pkg/url.go b/pkg/url.go index 9a29721..d8beefa 100644 --- a/pkg/url.go +++ b/pkg/url.go @@ -2,6 +2,7 @@ package pkg import ( urlpkg "net/url" + "path" "path/filepath" "regexp" "strings" @@ -52,6 +53,12 @@ func ParseURL(rawURL string, defaultHost string) (url *urlpkg.URL, err error) { url.Host = defaultHost } + // Don't use host when scheme is file://. The fragment detected as url.Host should be a first directory of url.Path + if url.Scheme == "file" && url.Host != "" { + url.Path = path.Join(url.Host, url.Path) + url.Host = "" + } + // Default to https when scheme is empty if url.Scheme == "" { url.Scheme = "https" @@ -60,18 +67,30 @@ func ParseURL(rawURL string, defaultHost string) (url *urlpkg.URL, err error) { return url, nil } -// URLToPath cleans up the URL and converts it into a path string. +// URLToPath cleans up the URL and converts it into a path string with correct separators for the current OS. // Eg, ssh://git@github.com:22/~user/repo.git => github.com/user/repo -func URLToPath(url *urlpkg.URL) (repoPath string) { - // Remove port numbers from host - repoHost := strings.Split(url.Host, ":")[0] +// +// If skipHost is true, it removes the host part from the path. +// Eg, ssh://git@github.com:22/~user/repo.git => user/repo +func URLToPath(url urlpkg.URL, skipHost bool) string { + // Remove port numbers from host. + url.Host = strings.Split(url.Host, ":")[0] - // Remove trailing ".git" from repo name - repoPath = filepath.Join(repoHost, url.Path) - repoPath = strings.TrimSuffix(repoPath, ".git") + // Remove tilde (~) char from username. + url.Path = strings.ReplaceAll(url.Path, "~", "") - // Remove tilde (~) char from username - repoPath = strings.ReplaceAll(repoPath, "~", "") + // Remove leading and trailing slashes (correct separator is added by the filepath.Join() below). + url.Path = strings.Trim(url.Path, "/") - return repoPath + // Remove trailing ".git" from repo name. + url.Path = strings.TrimSuffix(url.Path, ".git") + + // Replace slashes with separator correct for the current OS. + url.Path = strings.ReplaceAll(url.Path, "/", string(filepath.Separator)) + + if skipHost { + url.Host = "" + } + + return filepath.Join(url.Host, url.Path) } diff --git a/pkg/url_test.go b/pkg/url_test.go index c7329e3..9f12fd7 100644 --- a/pkg/url_test.go +++ b/pkg/url_test.go @@ -52,13 +52,58 @@ func TestURLParse(t *testing.T) { for _, test := range tests { url, err := ParseURL(test.in, cfg.Defaults[cfg.KeyDefaultHost]) if err != nil { - t.Errorf("Error parsing Path: %+v", err) + t.Fatalf("got error: %+v", err) } - got := URLToPath(url) + got := URLToPath(*url, false) if got != test.want { - t.Errorf("Wrong result of parsing Path: %s, got: %s; wantBranch: %s", test.in, got, test.want) + t.Errorf("wrong result for %q; expected %q; got %q", test.in, test.want, got) + } + } +} +func TestURLParseSkipHost(t *testing.T) { + tests := []struct { + in string + want string + }{ + {"ssh://github.com/grdl/git-get.git", "grdl/git-get"}, + {"ssh://user@github.com/grdl/git-get.git", "grdl/git-get"}, + {"ssh://user@github.com:1234/grdl/git-get.git", "grdl/git-get"}, + {"ssh://user@github.com/~user/grdl/git-get.git", "user/grdl/git-get"}, + {"git+ssh://github.com/grdl/git-get.git", "grdl/git-get"}, + {"git@github.com:grdl/git-get.git", "grdl/git-get"}, + {"git@github.com:/~user/grdl/git-get.git", "user/grdl/git-get"}, + {"git://github.com/grdl/git-get.git", "grdl/git-get"}, + {"git://github.com/~user/grdl/git-get.git", "user/grdl/git-get"}, + {"https://github.com/grdl/git-get.git", "grdl/git-get"}, + {"http://github.com/grdl/git-get.git", "grdl/git-get"}, + {"https://github.com/grdl/git-get", "grdl/git-get"}, + {"https://github.com/git-get.git", "git-get"}, + {"https://github.com/git-get", "git-get"}, + {"https://github.com/grdl/sub/path/git-get.git", "grdl/sub/path/git-get"}, + {"https://github.com:1234/grdl/git-get.git", "grdl/git-get"}, + {"https://github.com/grdl/git-get.git/", "grdl/git-get"}, + {"https://github.com/grdl/git-get/", "grdl/git-get"}, + {"https://github.com/grdl/git-get/////", "grdl/git-get"}, + {"https://github.com/grdl/git-get.git/////", "grdl/git-get"}, + {"ftp://github.com/grdl/git-get.git", "grdl/git-get"}, + {"ftps://github.com/grdl/git-get.git", "grdl/git-get"}, + {"rsync://github.com/grdl/git-get.git", "grdl/git-get"}, + {"local/grdl/git-get/", "local/grdl/git-get"}, + {"file://local/grdl/git-get", "local/grdl/git-get"}, + } + + for _, test := range tests { + url, err := ParseURL(test.in, cfg.Defaults[cfg.KeyDefaultHost]) + if err != nil { + t.Fatalf("got error: %+v", err) + } + + got := URLToPath(*url, true) + + if got != test.want { + t.Errorf("wrong result for %q; expected %q; got %q", test.in, test.want, got) } } } @@ -73,10 +118,10 @@ func TestInvalidURLParse(t *testing.T) { //"git@github.com:1234:grdl/git-get.git", } - for _, in := range invalidURLs { - got, err := ParseURL(in, cfg.Defaults[cfg.KeyDefaultHost]) + for _, test := range invalidURLs { + got, err := ParseURL(test, cfg.Defaults[cfg.KeyDefaultHost]) if err == nil { - t.Errorf("Wrong result of parsing invalid Path: %s, got: %s, wantBranch: error", in, got) + t.Errorf("expected error; got %q", got) } } }