Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance svn and git-svn support #248

Merged
merged 6 commits into from
Dec 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion remote_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
}

Expand Down
99 changes: 96 additions & 3 deletions vcs.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package main

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/motemen/ghq/cmdutil"
)
Expand Down Expand Up @@ -84,9 +90,46 @@ 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)/[^/]+$`)

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

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 {
vg.dir = svnBase(vg.dir)
dir, _ := filepath.Split(vg.dir)
err := os.MkdirAll(dir, 0755)
if err != nil {
Expand All @@ -95,13 +138,20 @@ 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 != "" {
copied := *vg.url
remote = &copied
remote.Path = svnBase(remote.Path)
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)

Expand All @@ -113,23 +163,66 @@ 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 {
orig := vg.dir
vg.dir = svnBase(vg.dir)
standard := orig == vg.dir

dir, _ := filepath.Split(vg.dir)
err := os.MkdirAll(dir, 0755)
if err != nil {
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
if vg.branch != "" {
copied := *remote
remote = &copied
remote.Path = svnBase(remote.Path)
remote.Path += "/branches/" + url.PathEscape(vg.branch)
standard = false
} else if standard {
copied := *remote
copied.Path += trunk
info, err := getSvnInfo(copied.String())
if err == nil {
args = append(args, "-s")
svnInfo = info
} else {
standard = false
}
}

return run(vg.silent)("git", "svn", "clone", remote.String(), vg.dir)
if vg.shallow {
if svnInfo == "" {
info, err := getSvnInfo(remote.String())
if err != nil {
return err
}
svnInfo = info
}
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")
Expand Down
34 changes: 29 additions & 5 deletions vcs_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
Expand All @@ -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
}

Expand Down Expand Up @@ -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{
Expand All @@ -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 {
Expand Down Expand Up @@ -140,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 {
Expand Down