Skip to content

Commit 0d0c788

Browse files
committed
git url: support query form
Fix issue 4905, but the syntax differs from the original proposal. The document will be added to https://github.com/docker/docs/blob/main/content/manuals/build/concepts/context.md#url-fragments Signed-off-by: Akihiro Suda <[email protected]>
1 parent b842feb commit 0d0c788

File tree

5 files changed

+171
-21
lines changed

5 files changed

+171
-21
lines changed

util/gitutil/git_ref.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,14 @@ func ParseGitRef(ref string) (*GitRef, error) {
6161
return nil, cerrdefs.ErrInvalidArgument
6262
} else if strings.HasPrefix(ref, "github.com/") {
6363
res.IndistinguishableFromLocal = true // Deprecated
64-
remote = fromURL(&url.URL{
64+
remote, err = fromURL(&url.URL{
6565
Scheme: "https",
6666
Host: "github.com",
6767
Path: strings.TrimPrefix(ref, "github.com/"),
6868
})
69+
if err != nil {
70+
return nil, err
71+
}
6972
} else {
7073
remote, err = ParseURL(ref)
7174
if errors.Is(err, ErrUnknownProtocol) {

util/gitutil/git_ref_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ func TestParseGitRef(t *testing.T) {
138138
ref: ".git",
139139
expected: nil,
140140
},
141+
{
142+
ref: "https://github.com/docker/docker.git?ref=v1.0.0&subdir=/subdir",
143+
expected: &GitRef{
144+
Remote: "https://github.com/docker/docker.git",
145+
ShortName: "docker",
146+
Commit: "v1.0.0",
147+
SubDir: "/subdir",
148+
},
149+
},
141150
}
142151
for _, tt := range cases {
143152
t.Run(tt.ref, func(t *testing.T) {

util/gitutil/git_url.go

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

33
import (
4+
"fmt"
45
"net/url"
56
"regexp"
67
"strings"
@@ -66,12 +67,63 @@ type GitURLOpts struct {
6667

6768
// parseOpts splits a git URL fragment into its respective git
6869
// reference and subdirectory components.
69-
func parseOpts(fragment string) *GitURLOpts {
70-
if fragment == "" {
71-
return nil
70+
func parseOpts(fragment string, query url.Values) (*GitURLOpts, error) {
71+
if fragment == "" && len(query) == 0 {
72+
return nil, nil
7273
}
73-
ref, subdir, _ := strings.Cut(fragment, ":")
74-
return &GitURLOpts{Ref: ref, Subdir: subdir}
74+
opts := &GitURLOpts{}
75+
if fragment != "" {
76+
opts.Ref, opts.Subdir, _ = strings.Cut(fragment, ":")
77+
}
78+
var tag, branch string
79+
for k, v := range query {
80+
switch len(v) {
81+
case 0:
82+
return nil, fmt.Errorf("query %q has no value", k)
83+
case 1:
84+
if v[0] == "" {
85+
return nil, fmt.Errorf("query %q has no value", k)
86+
}
87+
// NOP
88+
default:
89+
return nil, fmt.Errorf("query %q has multiple values", k)
90+
}
91+
switch k {
92+
case "ref":
93+
if opts.Ref != "" && opts.Ref != v[0] {
94+
return nil, fmt.Errorf("ref conflicts: %q vs %q", opts.Ref, v[0])
95+
}
96+
opts.Ref = v[0]
97+
case "tag":
98+
tag = v[0]
99+
case "branch":
100+
branch = v[0]
101+
case "subdir":
102+
if opts.Subdir != "" && opts.Subdir != v[0] {
103+
return nil, fmt.Errorf("subdir conflicts: %q vs %q", opts.Subdir, v[0])
104+
}
105+
opts.Subdir = v[0]
106+
default:
107+
return nil, fmt.Errorf("unexpected query %q", k)
108+
}
109+
}
110+
if tag != "" {
111+
if opts.Ref != "" {
112+
return nil, errors.New("tag conflicts with ref")
113+
}
114+
opts.Ref = "refs/tags/" + tag
115+
}
116+
if branch != "" {
117+
if tag != "" {
118+
// TODO: consider allowing this, when the tag actually exists on the branch
119+
return nil, errors.New("branch conflicts with tag")
120+
}
121+
if opts.Ref != "" {
122+
return nil, errors.New("branch conflicts with ref")
123+
}
124+
opts.Ref = "refs/heads/" + branch
125+
}
126+
return opts, nil
75127
}
76128

77129
// ParseURL parses a BuildKit-style Git URL (that may contain additional
@@ -86,11 +138,11 @@ func ParseURL(remote string) (*GitURL, error) {
86138
if err != nil {
87139
return nil, err
88140
}
89-
return fromURL(url), nil
141+
return fromURL(url)
90142
}
91143

92144
if url, err := sshutil.ParseSCPStyleURL(remote); err == nil {
93-
return fromSCPStyleURL(url), nil
145+
return fromSCPStyleURL(url)
94146
}
95147

96148
return nil, ErrUnknownProtocol
@@ -105,28 +157,38 @@ func IsGitTransport(remote string) bool {
105157
return sshutil.IsImplicitSSHTransport(remote)
106158
}
107159

108-
func fromURL(url *url.URL) *GitURL {
160+
func fromURL(url *url.URL) (*GitURL, error) {
109161
withoutOpts := *url
110162
withoutOpts.Fragment = ""
163+
withoutOpts.RawQuery = ""
164+
opts, err := parseOpts(url.Fragment, url.Query())
165+
if err != nil {
166+
return nil, err
167+
}
111168
return &GitURL{
112169
Scheme: url.Scheme,
113170
User: url.User,
114171
Host: url.Host,
115172
Path: url.Path,
116-
Opts: parseOpts(url.Fragment),
173+
Opts: opts,
117174
Remote: withoutOpts.String(),
118-
}
175+
}, nil
119176
}
120177

121-
func fromSCPStyleURL(url *sshutil.SCPStyleURL) *GitURL {
178+
func fromSCPStyleURL(url *sshutil.SCPStyleURL) (*GitURL, error) {
122179
withoutOpts := *url
123180
withoutOpts.Fragment = ""
181+
withoutOpts.Query = nil
182+
opts, err := parseOpts(url.Fragment, url.Query)
183+
if err != nil {
184+
return nil, err
185+
}
124186
return &GitURL{
125187
Scheme: SSHProtocol,
126188
User: url.User,
127189
Host: url.Host,
128190
Path: url.Path,
129-
Opts: parseOpts(url.Fragment),
191+
Opts: opts,
130192
Remote: withoutOpts.String(),
131-
}
193+
}, nil
132194
}

util/gitutil/git_url_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,65 @@ func TestParseURL(t *testing.T) {
151151
Path: "/moby/buildkit",
152152
},
153153
},
154+
{
155+
url: "https://github.com/moby/buildkit?ref=v1.0.0&subdir=/subdir",
156+
result: GitURL{
157+
Scheme: HTTPSProtocol,
158+
Host: "github.com",
159+
Path: "/moby/buildkit",
160+
Opts: &GitURLOpts{Ref: "v1.0.0", Subdir: "/subdir"},
161+
},
162+
},
163+
{
164+
url: "https://github.com/moby/buildkit?subdir=/subdir#v1.0.0",
165+
result: GitURL{
166+
Scheme: HTTPSProtocol,
167+
Host: "github.com",
168+
Path: "/moby/buildkit",
169+
Opts: &GitURLOpts{Ref: "v1.0.0", Subdir: "/subdir"},
170+
},
171+
},
172+
{
173+
url: "https://github.com/moby/buildkit?tag=v1.0.0",
174+
result: GitURL{
175+
Scheme: HTTPSProtocol,
176+
Host: "github.com",
177+
Path: "/moby/buildkit",
178+
Opts: &GitURLOpts{Ref: "refs/tags/v1.0.0"},
179+
},
180+
},
181+
{
182+
url: "https://github.com/moby/buildkit?branch=v1.0",
183+
result: GitURL{
184+
Scheme: HTTPSProtocol,
185+
Host: "github.com",
186+
Path: "/moby/buildkit",
187+
Opts: &GitURLOpts{Ref: "refs/heads/v1.0"},
188+
},
189+
},
190+
{
191+
url: "https://github.com/moby/buildkit?ref=v1.0.0#v1.2.3",
192+
err: true,
193+
},
194+
{
195+
url: "https://github.com/moby/buildkit?ref=v1.0.0&tag=v1.2.3",
196+
err: true,
197+
},
198+
{
199+
// TODO: consider allowing this, when the tag actually exists on the branch
200+
url: "https://github.com/moby/buildkit?tag=v1.0.0&branch=v1.0",
201+
err: true,
202+
},
203+
{
204+
url: "[email protected]:moby/buildkit.git?subdir=/subdir#v1.0.0",
205+
result: GitURL{
206+
Scheme: SSHProtocol,
207+
Host: "github.com",
208+
Path: "moby/buildkit.git",
209+
User: url.User("git"),
210+
Opts: &GitURLOpts{Ref: "v1.0.0", Subdir: "/subdir"},
211+
},
212+
},
154213
}
155214
for _, test := range tests {
156215
t.Run(test.url, func(t *testing.T) {

util/sshutil/scpurl.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"regexp"
88
)
99

10-
var gitSSHRegex = regexp.MustCompile("^([a-zA-Z0-9-_]+)@([a-zA-Z0-9-.]+):(.*?)(?:#(.*))?$")
10+
var gitSSHRegex = regexp.MustCompile(`^([a-zA-Z0-9-_]+)@([a-zA-Z0-9-.]+):(.*?)(?:\?(.*?))?(?:#(.*))?$`)
1111

1212
func IsImplicitSSHTransport(s string) bool {
1313
return gitSSHRegex.MatchString(s)
@@ -18,6 +18,7 @@ type SCPStyleURL struct {
1818
Host string
1919

2020
Path string
21+
Query url.Values
2122
Fragment string
2223
}
2324

@@ -26,18 +27,34 @@ func ParseSCPStyleURL(raw string) (*SCPStyleURL, error) {
2627
if matches == nil {
2728
return nil, errors.New("invalid scp-style url")
2829
}
30+
31+
rawQuery := matches[4]
32+
vals := url.Values{}
33+
if rawQuery != "" {
34+
var err error
35+
vals, err = url.ParseQuery(rawQuery)
36+
if err != nil {
37+
return nil, fmt.Errorf("invalid query in scp-style url: %w", err)
38+
}
39+
}
40+
2941
return &SCPStyleURL{
3042
User: url.User(matches[1]),
3143
Host: matches[2],
3244
Path: matches[3],
33-
Fragment: matches[4],
45+
Query: vals,
46+
Fragment: matches[5],
3447
}, nil
3548
}
3649

37-
func (url *SCPStyleURL) String() string {
38-
base := fmt.Sprintf("%s@%s:%s", url.User.String(), url.Host, url.Path)
39-
if url.Fragment == "" {
40-
return base
50+
func (u *SCPStyleURL) String() string {
51+
s := fmt.Sprintf("%s@%s:%s", u.User.String(), u.Host, u.Path)
52+
53+
if len(u.Query) > 0 {
54+
s += "?" + u.Query.Encode()
55+
}
56+
if u.Fragment != "" {
57+
s += "#" + u.Fragment
4158
}
42-
return base + "#" + url.Fragment
59+
return s
4360
}

0 commit comments

Comments
 (0)