diff --git a/cmd_create.go b/cmd_create.go index 08aeb15..ae644b7 100644 --- a/cmd_create.go +++ b/cmd_create.go @@ -13,6 +13,7 @@ func doCreate(c *cli.Context) error { name = c.Args().First() vcs = c.String("vcs") w = c.App.Writer + bare = c.Bool("bare") ) if name == "" { @@ -24,7 +25,7 @@ func doCreate(c *cli.Context) error { return err } - localRepo, err := LocalRepositoryFromURL(u) + localRepo, err := LocalRepositoryFromURL(u, bare) if err != nil { return err } diff --git a/cmd_get.go b/cmd_get.go index da1bab6..82e17d0 100644 --- a/cmd_get.go +++ b/cmd_get.go @@ -81,7 +81,7 @@ func doGet(c *cli.Context) error { return err } if andLook && firstArg != "" { - return look(firstArg) + return look(firstArg, g.bare) } return nil } @@ -121,7 +121,7 @@ func detectShell() string { return "/bin/sh" } -func look(name string) error { +func look(name string, bare bool) error { var ( reposFound []*LocalRepository mu sync.Mutex @@ -138,7 +138,7 @@ func look(name string) error { if len(reposFound) == 0 { if url, err := newURL(name, false, false); err == nil { - repo, err := LocalRepositoryFromURL(url) + repo, err := LocalRepositoryFromURL(url, bare) if err != nil { return err } diff --git a/cmd_get_test.go b/cmd_get_test.go index 222752d..607f819 100644 --- a/cmd_get_test.go +++ b/cmd_get_test.go @@ -206,7 +206,7 @@ func TestCommandGet(t *testing.T) { }, { name: "bare", scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) { - localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo") + localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo.git") app.Run([]string{"", "get", "--bare", "motemen/ghq-test-repo"}) @@ -263,13 +263,13 @@ func TestLook(t *testing.T) { t.Errorf("lastCmd.Env[len(lastCmd.Env)-1]: got: %s, expect: %s", gotEnv, expectEnv) } - err = look("github.com/motemen/_unknown") + err = look("github.com/motemen/_unknown", false) expect := "no repository found" if !strings.HasPrefix(fmt.Sprintf("%s", err), expect) { t.Errorf("error should has prefix %q, but: %s", expect, err) } - err = look("gobump") + err = look("gobump", false) expect = "More than one repositories are found; Try more precise name" if !strings.HasPrefix(fmt.Sprintf("%s", err), expect) { t.Errorf("error should has prefix %q, but: %s", expect, err) @@ -277,6 +277,52 @@ func TestLook(t *testing.T) { }) } +func TestBareLook(t *testing.T) { + withFakeGitBackend(t, func(t *testing.T, tmproot string, _ *_cloneArgs, _ *_updateArgs) { + os.MkdirAll(filepath.Join(tmproot, "github.com", "motemen", "ghq.git"), 0o755) + os.MkdirAll(filepath.Join(tmproot, "github.com", "motemen", "gobump", ".git"), 0o755) + defer func(orig func(cmd *exec.Cmd) error) { + cmdutil.CommandRunner = orig + }(cmdutil.CommandRunner) + var lastCmd *exec.Cmd + cmdutil.CommandRunner = func(cmd *exec.Cmd) error { + lastCmd = cmd + return nil + } + sh := detectShell() + + err := newApp().Run([]string{"", "get", "--bare", "--look", "https://github.com/motemen/ghq.git"}) + if err != nil { + t.Errorf("error should be nil, but: %s", err) + } + + if !reflect.DeepEqual(lastCmd.Args, []string{sh}) { + t.Errorf("lastCmd.Args: got: %v, expect: %v", lastCmd.Args, []string{sh}) + } + dir := filepath.Join(tmproot, "github.com", "motemen", "ghq.git") + if filepath.Clean(lastCmd.Dir) != dir { + t.Errorf("lastCmd.Dir: got: %s, expect: %s", lastCmd.Dir, dir) + } + gotEnv := lastCmd.Env[len(lastCmd.Env)-1] + expectEnv := "GHQ_LOOK=github.com/motemen/ghq.git" + if gotEnv != expectEnv { + t.Errorf("lastCmd.Env[len(lastCmd.Env)-1]: got: %s, expect: %s", gotEnv, expectEnv) + } + + err = look("github.com/motemen/ghq", false) + expect := "no repository found" + if !strings.HasPrefix(fmt.Sprintf("%s", err), expect) { + t.Errorf("error should has prefix %q, but: %s", expect, err) + } + + err = look("github.com/motemen/gobump.git", true) + expect = "no repository found" + if !strings.HasPrefix(fmt.Sprintf("%s", err), expect) { + t.Errorf("error should has prefix %q, but: %s", expect, err) + } + }) +} + func TestDoGet_bulk(t *testing.T) { in := []string{ "github.com/x-motemen/ghq", diff --git a/cmd_list.go b/cmd_list.go index 8dcae01..9d0d48d 100644 --- a/cmd_list.go +++ b/cmd_list.go @@ -18,6 +18,7 @@ func doList(c *cli.Context) error { vcsBackend = c.String("vcs") printFullPaths = c.Bool("full-path") printUniquePaths = c.Bool("unique") + bare = c.Bool("bare") ) filterByQuery := func(_ *LocalRepository) bool { @@ -26,7 +27,7 @@ func doList(c *cli.Context) error { if query != "" { if hasSchemePattern.MatchString(query) || scpLikeURLPattern.MatchString(query) { if url, err := newURL(query, false, false); err == nil { - if repo, err := LocalRepositoryFromURL(url); err == nil { + if repo, err := LocalRepositoryFromURL(url, bare); err == nil { query = filepath.ToSlash(repo.RelPath) } } diff --git a/commands.go b/commands.go index aae1c2f..fe94a45 100644 --- a/commands.go +++ b/commands.go @@ -54,6 +54,7 @@ var commandList = &cli.Command{ &cli.StringFlag{Name: "vcs", Usage: "Specify `vcs` backend for matching"}, &cli.BoolFlag{Name: "full-path", Aliases: []string{"p"}, Usage: "Print full paths"}, &cli.BoolFlag{Name: "unique", Usage: "Print unique subpaths"}, + &cli.BoolFlag{Name: "bare", Usage: "Query bare repositories"}, }, } @@ -72,6 +73,7 @@ var commandCreate = &cli.Command{ Action: doCreate, Flags: []cli.Flag{ &cli.StringFlag{Name: "vcs", Usage: "Specify `vcs` backend explicitly"}, + &cli.BoolFlag{Name: "bare", Usage: "Create a bare repository"}, }, } diff --git a/getter.go b/getter.go index af64ea9..b04a429 100644 --- a/getter.go +++ b/getter.go @@ -46,7 +46,7 @@ func (g *getter) get(argURL string) error { // If isShallow is true, does shallow cloning. (no effect if already cloned or the VCS is Mercurial and git-svn) func (g *getter) getRemoteRepository(remote RemoteRepository, branch string) error { remoteURL := remote.URL() - local, err := LocalRepositoryFromURL(remoteURL) + local, err := LocalRepositoryFromURL(remoteURL, g.bare) if err != nil { return err } @@ -89,6 +89,10 @@ func (g *getter) getRemoteRepository(remote RemoteRepository, branch string) err localRepoRoot = filepath.Join(local.RootPath, remoteURL.Hostname(), l) } + if g.bare { + localRepoRoot = localRepoRoot + ".git" + } + if remoteURL.Scheme == "codecommit" { repoURL, _ = url.Parse(remoteURL.Opaque) } @@ -110,11 +114,17 @@ func (g *getter) getRemoteRepository(remote RemoteRepository, branch string) err if vcs == nil { return fmt.Errorf("failed to detect VCS for %q", fpath) } + repoURL := remoteURL + if remoteURL.Scheme == "codecommit" { + repoURL, _ = url.Parse(remoteURL.Opaque) + } if getRepoLock(localRepoRoot) { return vcs.Update(&vcsGetOption{ + url: repoURL, dir: localRepoRoot, silent: g.silent, recursive: g.recursive, + bare: g.bare, }) } return nil diff --git a/local_repository.go b/local_repository.go index 8b03e50..e883ae5 100644 --- a/local_repository.go +++ b/local_repository.go @@ -72,12 +72,17 @@ func LocalRepositoryFromFullPath(fullPath string, backend *VCSBackend) (*LocalRe } // LocalRepositoryFromURL resolve LocalRepository from URL -func LocalRepositoryFromURL(remoteURL *url.URL) (*LocalRepository, error) { +func LocalRepositoryFromURL(remoteURL *url.URL, bare bool) (*LocalRepository, error) { pathParts := append( []string{remoteURL.Hostname()}, strings.Split(remoteURL.Path, "/")..., ) relPath := strings.TrimSuffix(filepath.Join(pathParts...), ".git") pathParts[len(pathParts)-1] = strings.TrimSuffix(pathParts[len(pathParts)-1], ".git") + if bare { + // Force to append ".git" even if remoteURL does not end with ".git". + relPath = relPath + ".git" + pathParts[len(pathParts)-1] = pathParts[len(pathParts)-1] + ".git" + } var ( localRepository *LocalRepository @@ -232,6 +237,9 @@ func findVCSBackend(fpath, vcs string) *VCSBackend { if !ok { return nil } + if vcsBackend == GitBackend && strings.HasSuffix(fpath, ".git") { + return vcsBackend + } for _, d := range vcsBackend.Contents { if _, err := os.Stat(filepath.Join(fpath, d)); err == nil { return vcsBackend @@ -239,6 +247,9 @@ func findVCSBackend(fpath, vcs string) *VCSBackend { } return nil } + if strings.HasSuffix(fpath, ".git") { + return GitBackend + } for _, d := range vcsContents { if _, err := os.Stat(filepath.Join(fpath, d)); err == nil { return vcsContentsMap[d] diff --git a/local_repository_test.go b/local_repository_test.go index 1888612..bf456a8 100644 --- a/local_repository_test.go +++ b/local_repository_test.go @@ -123,7 +123,7 @@ func TestNewLocalRepository(t *testing.T) { defer func(orig string) { _home = orig }(_home) _home = "" homeOnce = &sync.Once{} - r, err := LocalRepositoryFromURL(mustParseURL(tc.url)) + r, err := LocalRepositoryFromURL(mustParseURL(tc.url), false) if err != nil { t.Errorf("error should be nil but: %s", err) } @@ -249,6 +249,15 @@ func TestFindVCSBackend(t *testing.T) { setup func(t *testing.T) (string, string) expect *VCSBackend }{{ + name: "git-bare", + setup: func(t *testing.T) (string, string) { + dir := newTempDir(t) + dir = dir + ".git" + os.MkdirAll(dir, 0o755) + return dir, "" + }, + expect: GitBackend, + }, { name: "git", setup: func(t *testing.T) (string, string) { dir := newTempDir(t) diff --git a/vcs.go b/vcs.go index 3eb5113..db63149 100644 --- a/vcs.go +++ b/vcs.go @@ -78,6 +78,9 @@ var GitBackend = &VCSBackend{ if _, err := os.Stat(filepath.Join(vg.dir, ".git/svn")); err == nil { return GitsvnBackend.Update(vg) } + if vg.bare { + return runInDir(true)(vg.dir, "git", "fetch", vg.url.String(), "*:*") + } err := runInDir(true)(vg.dir, "git", "rev-parse", "@{upstream}") if err != nil { err := runInDir(vg.silent)(vg.dir, "git", "fetch") @@ -96,7 +99,11 @@ var GitBackend = &VCSBackend{ return nil }, Init: func(dir string) error { - return cmdutil.RunInDir(dir, "git", "init") + args := []string{"init"} + if strings.HasSuffix(dir, ".git") { + args = append(args, "--bare") + } + return cmdutil.RunInDir(dir, "git", args...) }, Contents: []string{".git"}, }