From e74ca417e1b285cb6fc1b73365bbaec41697a3ea Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Dec 2019 01:22:09 +0900 Subject: [PATCH 1/6] svn --depth immediates --- vcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcs.go b/vcs.go index 45df9369..7421b2dc 100644 --- a/vcs.go +++ b/vcs.go @@ -95,7 +95,7 @@ var SubversionBackend = &VCSBackend{ args := []string{"checkout"} if vg.shallow { - args = append(args, "--depth", "1") + args = append(args, "--depth", "immediates") } remote := vg.url if vg.branch != "" { From 6f9d4e587bc9ebab6e9c02e3217a6ca7d9558c8c Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Dec 2019 02:43:48 +0900 Subject: [PATCH 2/6] handle trunk on svn --- vcs.go | 32 ++++++++++++++++++++++++++++++++ vcs_test.go | 12 ++++++++---- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/vcs.go b/vcs.go index 7421b2dc..ad24b921 100644 --- a/vcs.go +++ b/vcs.go @@ -5,6 +5,8 @@ import ( "net/url" "os" "path/filepath" + "regexp" + "strings" "github.com/motemen/ghq/cmdutil" ) @@ -84,9 +86,30 @@ var GitBackend = &VCSBackend{ Contents: []string{".git"}, } +var svnReg = regexp.MustCompile(`/(?:tags|branches)/[^/]+$`) + +func replaceOnce(reg *regexp.Regexp, str, replace string) string { + replaced := false + return reg.ReplaceAllStringFunc(str, func(match string) string { + if replaced { + return match + } + replaced = true + return reg.ReplaceAllString(match, replace) + }) +} + +const trunk = "/trunk" + // SubversionBackend is the VCSBackend for subversion var SubversionBackend = &VCSBackend{ Clone: func(vg *vcsGetOption) error { + if strings.HasSuffix(vg.dir, trunk) { + vg.dir = strings.TrimSuffix(vg.dir, trunk) + } else { + vg.dir = replaceOnce(svnReg, vg.dir, "") + } + dir, _ := filepath.Split(vg.dir) err := os.MkdirAll(dir, 0755) if err != nil { @@ -101,7 +124,16 @@ var SubversionBackend = &VCSBackend{ if vg.branch != "" { copied := *vg.url remote = &copied + if strings.HasSuffix(remote.Path, trunk) { + remote.Path = strings.TrimSuffix(remote.Path, trunk) + } remote.Path += "/branches/" + url.PathEscape(vg.branch) + } else if !strings.HasSuffix(remote.Path, trunk) { + copied := *vg.url + copied.Path += trunk + if err := cmdutil.RunSilently("svn", "info", copied.String()); err == nil { + remote = &copied + } } args = append(args, remote.String(), vg.dir) diff --git a/vcs_test.go b/vcs_test.go index a5af9fa4..d1652431 100644 --- a/vcs_test.go +++ b/vcs_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "os/exec" "path/filepath" @@ -23,6 +24,9 @@ func TestVCSBackend(t *testing.T) { }(cmdutil.CommandRunner) cmdutil.CommandRunner = func(cmd *exec.Cmd) error { _commands = append(_commands, cmd) + if reflect.DeepEqual(cmd.Args, []string{"svn", "info", "https://example.com/git/repo/trunk"}) { + return fmt.Errorf("[test] failed to svn info") + } return nil } @@ -74,13 +78,13 @@ func TestVCSBackend(t *testing.T) { name: "[git] recursive", f: func() error { return GitBackend.Clone(&vcsGetOption{ - url: remoteDummyURL, - dir: localDir, + url: remoteDummyURL, + dir: localDir, recursive: true, }) }, expect: []string{"git", "clone", "--recursive", remoteDummyURL.String(), localDir}, - },{ + }, { name: "[svn] checkout", f: func() error { return SubversionBackend.Clone(&vcsGetOption{ @@ -98,7 +102,7 @@ func TestVCSBackend(t *testing.T) { shallow: true, }) }, - expect: []string{"svn", "checkout", "--depth", "1", remoteDummyURL.String(), localDir}, + expect: []string{"svn", "checkout", "--depth", "immediates", remoteDummyURL.String(), localDir}, }, { name: "[svn] checkout specific branch", f: func() error { From 52bf3e5b7e96e34ad6f154eca9e76a3e3ee2d0e7 Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Dec 2019 03:56:27 +0900 Subject: [PATCH 3/6] shallow support on git-svn --- vcs.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++-- vcs_test.go | 22 +++++++++++++++++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/vcs.go b/vcs.go index ad24b921..8f8d301e 100644 --- a/vcs.go +++ b/vcs.go @@ -1,9 +1,13 @@ package main import ( + "bytes" "errors" + "fmt" + "io/ioutil" "net/url" "os" + "os/exec" "path/filepath" "regexp" "strings" @@ -145,23 +149,73 @@ var SubversionBackend = &VCSBackend{ Contents: []string{".svn"}, } +var svnLastRevReg = regexp.MustCompile(`(?m)^Last Changed Rev: (\d+)$`) + // GitsvnBackend is the VCSBackend for git-svn var GitsvnBackend = &VCSBackend{ - // git-svn seems not supporting shallow clone currently. Clone: func(vg *vcsGetOption) error { + standard := true + if strings.HasSuffix(vg.dir, trunk) { + standard = false + vg.dir = strings.TrimSuffix(vg.dir, trunk) + } else { + orig := vg.dir + vg.dir = replaceOnce(svnReg, vg.dir, "") + standard = orig == vg.dir + } + dir, _ := filepath.Split(vg.dir) err := os.MkdirAll(dir, 0755) if err != nil { return err } + + var svnInfo string + args := []string{"svn", "clone"} remote := vg.url if vg.branch != "" { copied := *remote remote = &copied remote.Path += "/branches/" + url.PathEscape(vg.branch) + standard = false + } else if standard { + copied := *remote + copied.Path += trunk + buf := &bytes.Buffer{} + cmd := exec.Command("svn", "info", copied.String()) + cmd.Stdout = buf + cmd.Stderr = ioutil.Discard + if err := cmdutil.RunCommand(cmd, true); err == nil { + args = append(args, "-s") + svnInfo = buf.String() + } else { + standard = false + } } - return run(vg.silent)("git", "svn", "clone", remote.String(), vg.dir) + if vg.shallow { + if svnInfo == "" { + copied := *remote + if standard { + copied.Path += trunk + } + buf := &bytes.Buffer{} + cmd := exec.Command("svn", "info", copied.String()) + cmd.Stdout = buf + cmd.Stderr = ioutil.Discard + if err := cmdutil.RunCommand(cmd, true); err != nil { + return err + } + svnInfo = buf.String() + } + m := svnLastRevReg.FindStringSubmatch(svnInfo) + if len(m) < 2 { + return fmt.Errorf("no revisions are taken from svn info output: %s", svnInfo) + } + args = append(args, fmt.Sprintf("-r%s:HEAD", m[1])) + } + args = append(args, remote.String(), vg.dir) + return run(vg.silent)("git", args...) }, Update: func(vg *vcsGetOption) error { return runInDir(vg.silent)(vg.dir, "git", "svn", "rebase") diff --git a/vcs_test.go b/vcs_test.go index d1652431..a247587a 100644 --- a/vcs_test.go +++ b/vcs_test.go @@ -144,13 +144,33 @@ func TestVCSBackend(t *testing.T) { }, { name: "[git-svn] clone shallow", f: func() error { + defer func(orig func(cmd *exec.Cmd) error) { + cmdutil.CommandRunner = orig + }(cmdutil.CommandRunner) + cmdutil.CommandRunner = func(cmd *exec.Cmd) error { + _commands = append(_commands, cmd) + if reflect.DeepEqual(cmd.Args, []string{"svn", "info", "https://example.com/git/repo/trunk"}) { + cmd.Stdout.Write([]byte(`Path: trunk +URL: https://svn.apache.org/repos/asf/subversion/trunk +Relative URL: ^/subversion/trunk +Repository Root: https://svn.apache.org/repos/asf +Repository UUID: 13f79535-47bb-0310-9956-ffa450edef68 +Revision: 1872085 +Node Kind: directory +Last Changed Author: julianfoad +Last Changed Rev: 1872031 +Last Changed Date: 2019-08-16 15:16:45 +0900 (Fri, 16 Aug 2019) +`)) + } + return nil + } return GitsvnBackend.Clone(&vcsGetOption{ url: remoteDummyURL, dir: localDir, shallow: true, }) }, - expect: []string{"git", "svn", "clone", remoteDummyURL.String(), localDir}, + expect: []string{"git", "svn", "clone", "-s", "-r1872031:HEAD", remoteDummyURL.String(), localDir}, }, { name: "[git-svn] clone specific branch", f: func() error { From 316435d9f423afc2ca718f810ffac4482f74e460 Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Dec 2019 13:34:27 +0900 Subject: [PATCH 4/6] utilize svnBase --- vcs.go | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/vcs.go b/vcs.go index 8f8d301e..17ef2325 100644 --- a/vcs.go +++ b/vcs.go @@ -90,6 +90,8 @@ var GitBackend = &VCSBackend{ Contents: []string{".git"}, } +const trunk = "/trunk" + var svnReg = regexp.MustCompile(`/(?:tags|branches)/[^/]+$`) func replaceOnce(reg *regexp.Regexp, str, replace string) string { @@ -103,17 +105,17 @@ func replaceOnce(reg *regexp.Regexp, str, replace string) string { }) } -const trunk = "/trunk" +func svnBase(p string) string { + if strings.HasSuffix(p, trunk) { + return strings.TrimSuffix(p, trunk) + } + return replaceOnce(svnReg, p, "") +} // SubversionBackend is the VCSBackend for subversion var SubversionBackend = &VCSBackend{ Clone: func(vg *vcsGetOption) error { - if strings.HasSuffix(vg.dir, trunk) { - vg.dir = strings.TrimSuffix(vg.dir, trunk) - } else { - vg.dir = replaceOnce(svnReg, vg.dir, "") - } - + vg.dir = svnBase(vg.dir) dir, _ := filepath.Split(vg.dir) err := os.MkdirAll(dir, 0755) if err != nil { @@ -128,9 +130,7 @@ var SubversionBackend = &VCSBackend{ if vg.branch != "" { copied := *vg.url remote = &copied - if strings.HasSuffix(remote.Path, trunk) { - remote.Path = strings.TrimSuffix(remote.Path, trunk) - } + remote.Path = svnBase(remote.Path) remote.Path += "/branches/" + url.PathEscape(vg.branch) } else if !strings.HasSuffix(remote.Path, trunk) { copied := *vg.url @@ -154,15 +154,9 @@ var svnLastRevReg = regexp.MustCompile(`(?m)^Last Changed Rev: (\d+)$`) // GitsvnBackend is the VCSBackend for git-svn var GitsvnBackend = &VCSBackend{ Clone: func(vg *vcsGetOption) error { - standard := true - if strings.HasSuffix(vg.dir, trunk) { - standard = false - vg.dir = strings.TrimSuffix(vg.dir, trunk) - } else { - orig := vg.dir - vg.dir = replaceOnce(svnReg, vg.dir, "") - standard = orig == vg.dir - } + orig := vg.dir + vg.dir = svnBase(vg.dir) + standard := orig == vg.dir dir, _ := filepath.Split(vg.dir) err := os.MkdirAll(dir, 0755) @@ -176,6 +170,7 @@ var GitsvnBackend = &VCSBackend{ if vg.branch != "" { copied := *remote remote = &copied + remote.Path = svnBase(remote.Path) remote.Path += "/branches/" + url.PathEscape(vg.branch) standard = false } else if standard { From da59340a7be2fb87af5b40dc5fb36b5c875a2d31 Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Dec 2019 13:43:57 +0900 Subject: [PATCH 5/6] refactor --- vcs.go | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/vcs.go b/vcs.go index 17ef2325..c04131da 100644 --- a/vcs.go +++ b/vcs.go @@ -164,6 +164,14 @@ var GitsvnBackend = &VCSBackend{ return err } + var getSvnInfo = func(u string) (string, error) { + buf := &bytes.Buffer{} + cmd := exec.Command("svn", "info", u) + cmd.Stdout = buf + cmd.Stderr = ioutil.Discard + err := cmdutil.RunCommand(cmd, true) + return buf.String(), err + } var svnInfo string args := []string{"svn", "clone"} remote := vg.url @@ -176,13 +184,10 @@ var GitsvnBackend = &VCSBackend{ } else if standard { copied := *remote copied.Path += trunk - buf := &bytes.Buffer{} - cmd := exec.Command("svn", "info", copied.String()) - cmd.Stdout = buf - cmd.Stderr = ioutil.Discard - if err := cmdutil.RunCommand(cmd, true); err == nil { + info, err := getSvnInfo(copied.String()) + if err == nil { args = append(args, "-s") - svnInfo = buf.String() + svnInfo = info } else { standard = false } @@ -190,18 +195,11 @@ var GitsvnBackend = &VCSBackend{ if vg.shallow { if svnInfo == "" { - copied := *remote - if standard { - copied.Path += trunk - } - buf := &bytes.Buffer{} - cmd := exec.Command("svn", "info", copied.String()) - cmd.Stdout = buf - cmd.Stderr = ioutil.Discard - if err := cmdutil.RunCommand(cmd, true); err != nil { + info, err := getSvnInfo(remote.String()) + if err != nil { return err } - svnInfo = buf.String() + svnInfo = info } m := svnLastRevReg.FindStringSubmatch(svnInfo) if len(m) < 2 { From a090c1b7a445bf39f6ed80782cebfc8311a558a9 Mon Sep 17 00:00:00 2001 From: Songmu Date: Mon, 30 Dec 2019 14:54:12 +0900 Subject: [PATCH 6/6] add document --- remote_repository.go | 7 ++++++- vcs.go | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/remote_repository.go b/remote_repository.go index 3eb4241c..93dadacf 100644 --- a/remote_repository.go +++ b/remote_repository.go @@ -127,6 +127,11 @@ func (repo *OtherRepository) VCS() (*VCSBackend, *url.URL) { return backend, repo.URL() } + mayBeSvn := strings.HasPrefix(repo.url.Host, "svn.") + if mayBeSvn && cmdutil.RunSilently("svn", "info", repo.url.String()) == nil { + return SubversionBackend, repo.URL() + } + // Detect VCS backend automatically if cmdutil.RunSilently("git", "ls-remote", repo.url.String()) == nil { return GitBackend, repo.URL() @@ -142,7 +147,7 @@ func (repo *OtherRepository) VCS() (*VCSBackend, *url.URL) { return MercurialBackend, repo.URL() } - if cmdutil.RunSilently("svn", "info", repo.url.String()) == nil { + if !mayBeSvn && cmdutil.RunSilently("svn", "info", repo.url.String()) == nil { return SubversionBackend, repo.URL() } diff --git a/vcs.go b/vcs.go index c04131da..ec4abdc6 100644 --- a/vcs.go +++ b/vcs.go @@ -90,6 +90,20 @@ var GitBackend = &VCSBackend{ Contents: []string{".git"}, } +/* +If the svn target is under standard svn directory structure, "ghq" canonicalizes the checkout path. +For example, all following targets are checked-out into `$(ghq root)/svn.example.com/proj/repo`. + +- svn.example.com/proj/repo +- svn.example.com/proj/repo/trunk +- svn.example.com/proj/repo/branches/featureN +- svn.example.com/proj/repo/tags/v1.0.1 + +Addition, when the svn target may be project root, "ghq" tries to checkout "/trunk". + +The checkout rule using "git-svn" also has the same behavior. +*/ + const trunk = "/trunk" var svnReg = regexp.MustCompile(`/(?:tags|branches)/[^/]+$`)