Skip to content

Commit bd1edc2

Browse files
authored
Merge pull request #129 from hashicorp/b/git-scp
Allow SCP-style URLs for Git
2 parents a0478b3 + f6c2109 commit bd1edc2

File tree

8 files changed

+196
-44
lines changed

8 files changed

+196
-44
lines changed

detect.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var Detectors []Detector
2323
func init() {
2424
Detectors = []Detector{
2525
new(GitHubDetector),
26+
new(GitDetector),
2627
new(BitBucketDetector),
2728
new(S3Detector),
2829
new(FileDetector),

detect_git.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package getter
2+
3+
// GitDetector implements Detector to detect Git SSH URLs such as
4+
// [email protected]:dir1/dir2 and converts them to proper URLs.
5+
type GitDetector struct{}
6+
7+
func (d *GitDetector) Detect(src, _ string) (string, bool, error) {
8+
if len(src) == 0 {
9+
return "", false, nil
10+
}
11+
12+
u, err := detectSSH(src)
13+
if err != nil {
14+
return "", true, err
15+
}
16+
if u == nil {
17+
return "", false, nil
18+
}
19+
20+
// We require the username to be "git" to assume that this is a Git URL
21+
if u.User.Username() != "git" {
22+
return "", false, nil
23+
}
24+
25+
return "git::" + u.String(), true, nil
26+
}

detect_git_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package getter
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestGitDetector(t *testing.T) {
8+
cases := []struct {
9+
Input string
10+
Output string
11+
}{
12+
{"[email protected]:hashicorp/foo.git", "git::ssh://[email protected]/hashicorp/foo.git"},
13+
{
14+
"[email protected]:org/project.git?ref=test-branch",
15+
"git::ssh://[email protected]/org/project.git?ref=test-branch",
16+
},
17+
{
18+
"[email protected]:hashicorp/foo.git//bar",
19+
"git::ssh://[email protected]/hashicorp/foo.git//bar",
20+
},
21+
{
22+
"[email protected]:hashicorp/foo.git?foo=bar",
23+
"git::ssh://[email protected]/hashicorp/foo.git?foo=bar",
24+
},
25+
{
26+
"[email protected]:org/project.git",
27+
"git::ssh://[email protected]/org/project.git",
28+
},
29+
{
30+
"[email protected]:org/project.git?ref=test-branch",
31+
"git::ssh://[email protected]/org/project.git?ref=test-branch",
32+
},
33+
{
34+
"[email protected]:org/project.git//module/a",
35+
"git::ssh://[email protected]/org/project.git//module/a",
36+
},
37+
{
38+
"[email protected]:org/project.git//module/a?ref=test-branch",
39+
"git::ssh://[email protected]/org/project.git//module/a?ref=test-branch",
40+
},
41+
}
42+
43+
pwd := "/pwd"
44+
f := new(GitDetector)
45+
for i, tc := range cases {
46+
output, ok, err := f.Detect(tc.Input, pwd)
47+
if err != nil {
48+
t.Fatalf("err: %s", err)
49+
}
50+
if !ok {
51+
t.Fatal("not ok")
52+
}
53+
54+
if output != tc.Output {
55+
t.Fatalf("%d: bad: %#v", i, output)
56+
}
57+
}
58+
}

detect_github.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ func (d *GitHubDetector) Detect(src, _ string) (string, bool, error) {
1717

1818
if strings.HasPrefix(src, "github.com/") {
1919
return d.detectHTTP(src)
20-
} else if strings.HasPrefix(src, "[email protected]:") {
21-
return d.detectSSH(src)
2220
}
2321

2422
return "", false, nil
@@ -47,27 +45,3 @@ func (d *GitHubDetector) detectHTTP(src string) (string, bool, error) {
4745

4846
return "git::" + url.String(), true, nil
4947
}
50-
51-
func (d *GitHubDetector) detectSSH(src string) (string, bool, error) {
52-
idx := strings.Index(src, ":")
53-
qidx := strings.Index(src, "?")
54-
if qidx == -1 {
55-
qidx = len(src)
56-
}
57-
58-
var u url.URL
59-
u.Scheme = "ssh"
60-
u.User = url.User("git")
61-
u.Host = "github.com"
62-
u.Path = src[idx+1 : qidx]
63-
if qidx < len(src) {
64-
q, err := url.ParseQuery(src[qidx+1:])
65-
if err != nil {
66-
return "", true, fmt.Errorf("error parsing GitHub SSH URL: %s", err)
67-
}
68-
69-
u.RawQuery = q.Encode()
70-
}
71-
72-
return "git::" + u.String(), true, nil
73-
}

detect_github_test.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,6 @@ func TestGitHubDetector(t *testing.T) {
2424
"github.com/hashicorp/foo.git?foo=bar",
2525
"git::https://github.com/hashicorp/foo.git?foo=bar",
2626
},
27-
28-
// SSH
29-
{"[email protected]:hashicorp/foo.git", "git::ssh://[email protected]/hashicorp/foo.git"},
30-
{
31-
"[email protected]:hashicorp/foo.git//bar",
32-
"git::ssh://[email protected]/hashicorp/foo.git//bar",
33-
},
34-
{
35-
"[email protected]:hashicorp/foo.git?foo=bar",
36-
"git::ssh://[email protected]/hashicorp/foo.git?foo=bar",
37-
},
3827
}
3928

4029
pwd := "/pwd"

detect_ssh.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package getter
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"regexp"
7+
"strings"
8+
)
9+
10+
// Note that we do not have an SSH-getter currently so this file serves
11+
// only to hold the detectSSH helper that is used by other detectors.
12+
13+
// sshPattern matches SCP-like SSH patterns (user@host:path)
14+
var sshPattern = regexp.MustCompile("^(?:([^@]+)@)?([^:]+):/?(.+)$")
15+
16+
// detectSSH determines if the src string matches an SSH-like URL and
17+
// converts it into a net.URL compatible string. This returns nil if the
18+
// string doesn't match the SSH pattern.
19+
//
20+
// This function is tested indirectly via detect_git_test.go
21+
func detectSSH(src string) (*url.URL, error) {
22+
matched := sshPattern.FindStringSubmatch(src)
23+
if matched == nil {
24+
return nil, nil
25+
}
26+
27+
user := matched[1]
28+
host := matched[2]
29+
path := matched[3]
30+
qidx := strings.Index(path, "?")
31+
if qidx == -1 {
32+
qidx = len(path)
33+
}
34+
35+
var u url.URL
36+
u.Scheme = "ssh"
37+
u.User = url.User(user)
38+
u.Host = host
39+
u.Path = path[0:qidx]
40+
if qidx < len(path) {
41+
q, err := url.ParseQuery(path[qidx+1:])
42+
if err != nil {
43+
return nil, fmt.Errorf("error parsing GitHub SSH URL: %s", err)
44+
}
45+
u.RawQuery = q.Encode()
46+
}
47+
48+
return &u, nil
49+
}

detect_test.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package getter
22

33
import (
4+
"fmt"
45
"testing"
56
)
67

@@ -37,21 +38,55 @@ func TestDetect(t *testing.T) {
3738
"git::https://github.com/hashicorp/consul.git",
3839
false,
3940
},
41+
{
42+
"git::https://[email protected]/foo/bar",
43+
"",
44+
"git::https://[email protected]/foo/bar",
45+
false,
46+
},
47+
{
48+
"git::https://[email protected]/foo/bar",
49+
"/bar",
50+
"git::https://[email protected]/foo/bar",
51+
false,
52+
},
4053
{
4154
"./foo/archive//*",
4255
"/bar",
4356
"file:///bar/foo/archive//*",
4457
false,
4558
},
59+
60+
// https://github.com/hashicorp/go-getter/pull/124
61+
{
62+
"git::ssh://[email protected]/dir1/dir2",
63+
"",
64+
"git::ssh://[email protected]/dir1/dir2",
65+
false,
66+
},
67+
{
68+
"git::[email protected]:dir1/dir2",
69+
"/foo",
70+
"git::ssh://[email protected]/dir1/dir2",
71+
false,
72+
},
73+
{
74+
"git::[email protected]:dir1/dir2",
75+
"",
76+
"git::ssh://[email protected]/dir1/dir2",
77+
false,
78+
},
4679
}
4780

4881
for i, tc := range cases {
49-
output, err := Detect(tc.Input, tc.Pwd, Detectors)
50-
if err != nil != tc.Err {
51-
t.Fatalf("%d: bad err: %s", i, err)
52-
}
53-
if output != tc.Output {
54-
t.Fatalf("%d: bad output: %s\nexpected: %s", i, output, tc.Output)
55-
}
82+
t.Run(fmt.Sprintf("%d %s", i, tc.Input), func(t *testing.T) {
83+
output, err := Detect(tc.Input, tc.Pwd, Detectors)
84+
if err != nil != tc.Err {
85+
t.Fatalf("%d: bad err: %s", i, err)
86+
}
87+
if output != tc.Output {
88+
t.Fatalf("%d: bad output: %s\nexpected: %s", i, output, tc.Output)
89+
}
90+
})
5691
}
5792
}

get_git.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,26 @@ func (g *GitGetter) Get(dst string, u *url.URL) error {
7878
}
7979
}
8080

81+
// For SSH-style URLs, if they use the SCP syntax of host:path, then
82+
// the URL will be mangled. We detect that here and correct the path.
83+
// Example: host:path/bar will turn into host/path/bar
84+
if u.Scheme == "ssh" {
85+
if idx := strings.Index(u.Host, ":"); idx > -1 {
86+
// Copy the URL so we don't modify the input
87+
var newU url.URL = *u
88+
u = &newU
89+
90+
// Path includes the part after the ':'.
91+
u.Path = u.Host[idx+1:] + u.Path
92+
if u.Path[0] != '/' {
93+
u.Path = "/" + u.Path
94+
}
95+
96+
// Host trims up to the :
97+
u.Host = u.Host[:idx]
98+
}
99+
}
100+
81101
// Clone or update the repository
82102
_, err := os.Stat(dst)
83103
if err != nil && !os.IsNotExist(err) {

0 commit comments

Comments
 (0)