11package gitutil
22
33import (
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}
0 commit comments