Skip to content

Commit d26c67a

Browse files
committed
git url: support CSV form
CSV parameters can be now specified after "##" in a git URL. e.g., https://github.com/user/repo.git##tag=mytag,branch=main,commit=cafebab,subdir=/dir tag, branch, and commit are exclusive. The follow-up commit will allow specifying tag and commit together. Fix issue 4905, but "source" in the original proposal was renamed to "subdir". The documents will be added in https://github.com/docker/docs/blob/main/content/manuals/build/concepts/context.md#url-fragments Signed-off-by: Akihiro Suda <[email protected]>
1 parent 37daea9 commit d26c67a

File tree

3 files changed

+112
-12
lines changed

3 files changed

+112
-12
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_url.go

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/moby/buildkit/util/sshutil"
99
"github.com/pkg/errors"
10+
"github.com/tonistiigi/go-csvvalue"
1011
)
1112

1213
const (
@@ -66,12 +67,59 @@ type GitURLFragment struct {
6667

6768
// splitGitFragment splits a git URL fragment into its respective git
6869
// reference and subdirectory components.
69-
func splitGitFragment(fragment string) *GitURLFragment {
70+
func splitGitFragment(fragment string) (*GitURLFragment, error) {
71+
if strings.HasPrefix(fragment, "#") {
72+
// Double-hash in the unparsed URL.
73+
// e.g., https://github.com/user/repo.git##tag=tag,subdir=/dir
74+
return splitGitFragmentCSVForm(fragment)
75+
}
76+
// Single-hash in the unparsed URL.
77+
// e.g., https://github.com/user/repo.git#branch_or_tag_or_commit:dir
7078
if fragment == "" {
71-
return nil
79+
return nil, nil
7280
}
7381
ref, subdir, _ := strings.Cut(fragment, ":")
74-
return &GitURLFragment{Ref: ref, Subdir: subdir}
82+
return &GitURLFragment{Ref: ref, Subdir: subdir}, nil
83+
}
84+
85+
func splitGitFragmentCSVForm(fragment string) (*GitURLFragment, error) {
86+
fragment = strings.TrimPrefix(fragment, "#")
87+
if fragment == "" {
88+
return nil, nil
89+
}
90+
fields, err := csvvalue.Fields(fragment, nil)
91+
if err != nil {
92+
return nil, errors.Wrapf(err, "failed to parse CSV %q", fragment)
93+
}
94+
95+
res := &GitURLFragment{}
96+
refs := make(map[string]string)
97+
for _, field := range fields {
98+
key, value, ok := strings.Cut(field, "=")
99+
if !ok {
100+
return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field)
101+
}
102+
key = strings.ToLower(key)
103+
switch key {
104+
case "tag", "branch", "commit":
105+
refs[key] = value
106+
case "subdir":
107+
res.Subdir = value
108+
default:
109+
return nil, errors.Errorf("unexpected key '%s' in '%s' (supported keys: tag, branch, commit, subdir)", key, field)
110+
}
111+
}
112+
if len(refs) > 0 {
113+
if len(refs) > 1 {
114+
// TODO: allow specifying tag and commit together https://github.com/moby/buildkit/issues/5871
115+
return nil, errors.New("tag, branch, and commit are exclusive")
116+
}
117+
for _, v := range refs {
118+
res.Ref = v
119+
break
120+
}
121+
}
122+
return res, nil
75123
}
76124

77125
// ParseURL parses a BuildKit-style Git URL (that may contain additional
@@ -86,11 +134,11 @@ func ParseURL(remote string) (*GitURL, error) {
86134
if err != nil {
87135
return nil, err
88136
}
89-
return fromURL(url), nil
137+
return fromURL(url)
90138
}
91139

92140
if url, err := sshutil.ParseSCPStyleURL(remote); err == nil {
93-
return fromSCPStyleURL(url), nil
141+
return fromSCPStyleURL(url)
94142
}
95143

96144
return nil, ErrUnknownProtocol
@@ -105,28 +153,37 @@ func IsGitTransport(remote string) bool {
105153
return sshutil.IsImplicitSSHTransport(remote)
106154
}
107155

108-
func fromURL(url *url.URL) *GitURL {
156+
func fromURL(url *url.URL) (*GitURL, error) {
109157
withoutFragment := *url
110158
withoutFragment.Fragment = ""
111-
return &GitURL{
159+
fragment, err := splitGitFragment(url.Fragment)
160+
if err != nil {
161+
return nil, errors.Wrapf(err, "failed to parse URL fragment %q", url.Fragment)
162+
}
163+
gitURL := &GitURL{
112164
Scheme: url.Scheme,
113165
User: url.User,
114166
Host: url.Host,
115167
Path: url.Path,
116-
Fragment: splitGitFragment(url.Fragment),
168+
Fragment: fragment,
117169
Remote: withoutFragment.String(),
118170
}
171+
return gitURL, nil
119172
}
120173

121-
func fromSCPStyleURL(url *sshutil.SCPStyleURL) *GitURL {
174+
func fromSCPStyleURL(url *sshutil.SCPStyleURL) (*GitURL, error) {
122175
withoutFragment := *url
123176
withoutFragment.Fragment = ""
177+
fragment, err := splitGitFragment(url.Fragment)
178+
if err != nil {
179+
return nil, errors.Wrapf(err, "failed to parse URL fragment %q", url.Fragment)
180+
}
124181
return &GitURL{
125182
Scheme: SSHProtocol,
126183
User: url.User,
127184
Host: url.Host,
128185
Path: url.Path,
129-
Fragment: splitGitFragment(url.Fragment),
186+
Fragment: fragment,
130187
Remote: withoutFragment.String(),
131-
}
188+
}, nil
132189
}

util/gitutil/git_url_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,46 @@ func TestParseURL(t *testing.T) {
151151
Path: "/moby/buildkit",
152152
},
153153
},
154+
{
155+
url: "https://github.com/moby/buildkit##tag=v1.0.0,subdir=/subdir",
156+
result: GitURL{
157+
Scheme: HTTPSProtocol,
158+
Host: "github.com",
159+
Path: "/moby/buildkit",
160+
Fragment: &GitURLFragment{Ref: "v1.0.0", Subdir: "/subdir"},
161+
},
162+
},
163+
{
164+
url: `https://github.com/moby/buildkit##"tag=tag,with,comma",subdir=/subdir`,
165+
result: GitURL{
166+
Scheme: HTTPSProtocol,
167+
Host: "github.com",
168+
Path: "/moby/buildkit",
169+
Fragment: &GitURLFragment{Ref: "tag,with,comma", Subdir: "/subdir"},
170+
},
171+
},
172+
{
173+
url: "https://github.com/moby/buildkit##branch=v1.0,subdir=subdir",
174+
result: GitURL{
175+
Scheme: HTTPSProtocol,
176+
Host: "github.com",
177+
Path: "/moby/buildkit",
178+
Fragment: &GitURLFragment{Ref: "v1.0", Subdir: "subdir"},
179+
},
180+
},
181+
{
182+
url: "https://github.com/moby/buildkit##commit=deadbeef,subdir=/subdir",
183+
result: GitURL{
184+
Scheme: HTTPSProtocol,
185+
Host: "github.com",
186+
Path: "/moby/buildkit",
187+
Fragment: &GitURLFragment{Ref: "deadbeef", Subdir: "/subdir"},
188+
},
189+
},
190+
{
191+
url: "https://github.com/moby/buildkit##tag=v1.0.0,branch=v1.0",
192+
err: true, // tag and branch are exclusive
193+
},
154194
}
155195
for _, test := range tests {
156196
t.Run(test.url, func(t *testing.T) {

0 commit comments

Comments
 (0)