github.com/nak3/source-to-image@v1.1.10-0.20180319140719-2ed55639898d/pkg/scm/git/url.go (about) 1 package git 2 3 import ( 4 "fmt" 5 "net/url" 6 "path/filepath" 7 "regexp" 8 "runtime" 9 "strings" 10 ) 11 12 // According to git-clone(1), a "Git URL" can be in one of three broad types: 13 // 1) A standards-compliant URL. 14 // a) The scheme may be followed by '://', 15 // e.g. https://github.com/openshift/origin, file:///foo/bar, etc. In 16 // this case, note that among other things, a standards-compliant file URL 17 // must have an empty host part, an absolute path and no backslashes. The 18 // Git for Windows URL parser accepts many non-compliant URLs, but we 19 // don't. 20 // b) Alternatively, the scheme may be followed by '::', in which case it is 21 // treated as an transport/opaque address pair, e.g. 22 // http::http://github.com/openshift/origin.git . 23 // 2) The "alternative scp-like syntax", including a ':' with no preceding '/', 24 // but not of the form C:... on Windows, e.g. 25 // git@github.com:openshift/origin, etc. 26 // 3) An OS-specific relative or absolute local file path, e.g. foo/bar, 27 // C:\foo\bar, etc. 28 // 29 // We extend all of the above URL types to additionally accept an optional 30 // appended #fragment, which is given to specify a git reference. 31 // 32 // The git client allows Git URL rewriting rules to be defined. The meaning of 33 // a Git URL cannot be 100% guaranteed without consulting the rewriting rules. 34 35 // URLType indicates the type of the URL (see above) 36 type URLType int 37 38 const ( 39 // URLTypeURL is the URL type (see above) 40 URLTypeURL URLType = iota 41 // URLTypeSCP is the SCP type (see above) 42 URLTypeSCP 43 // URLTypeLocal is the local type (see above) 44 URLTypeLocal 45 ) 46 47 // String returns a string representation of the URLType 48 func (t URLType) String() string { 49 switch t { 50 case URLTypeURL: 51 return "URLTypeURL" 52 case URLTypeSCP: 53 return "URLTypeSCP" 54 case URLTypeLocal: 55 return "URLTypeLocal" 56 } 57 panic("unknown URLType") 58 } 59 60 // GoString returns a Go string representation of the URLType 61 func (t URLType) GoString() string { 62 return t.String() 63 } 64 65 // URL represents a "Git URL" 66 type URL struct { 67 URL url.URL 68 Type URLType 69 } 70 71 var urlSchemeRegexp = regexp.MustCompile("(?i)^[a-z][-a-z0-9+.]*:") // matches scheme: according to RFC3986 72 var dosDriveRegexp = regexp.MustCompile("(?i)^[a-z]:") 73 var scpRegexp = regexp.MustCompile("^" + 74 "(?:([^@/]*)@)?" + // user@ (optional) 75 "([^/]*):" + // host: 76 "(.*)" + // path 77 "$") 78 79 func splitOnByte(s string, c byte) (string, string) { 80 if i := strings.IndexByte(s, c); i != -1 { 81 return s[:i], s[i+1:] 82 } 83 return s, "" 84 } 85 86 // Parse parses a "Git URL" 87 func Parse(rawurl string) (*URL, error) { 88 if urlSchemeRegexp.MatchString(rawurl) && 89 (runtime.GOOS != "windows" || !dosDriveRegexp.MatchString(rawurl)) { 90 u, err := url.Parse(rawurl) 91 if err != nil { 92 return nil, err 93 } 94 if u.Scheme == "file" && u.Opaque == "" { 95 if u.Host != "" { 96 return nil, fmt.Errorf("file url %q has non-empty host %q", rawurl, u.Host) 97 } 98 if runtime.GOOS == "windows" && (len(u.Path) == 0 || !filepath.IsAbs(u.Path[1:])) { 99 return nil, fmt.Errorf("file url %q has non-absolute path %q", rawurl, u.Path) 100 } 101 } 102 103 return &URL{ 104 URL: *u, 105 Type: URLTypeURL, 106 }, nil 107 } 108 109 s, fragment := splitOnByte(rawurl, '#') 110 111 if m := scpRegexp.FindStringSubmatch(s); m != nil && 112 (runtime.GOOS != "windows" || !dosDriveRegexp.MatchString(s)) { 113 u := &url.URL{ 114 Host: m[2], 115 Path: m[3], 116 Fragment: fragment, 117 } 118 if m[1] != "" { 119 u.User = url.User(m[1]) 120 } 121 122 return &URL{ 123 URL: *u, 124 Type: URLTypeSCP, 125 }, nil 126 } 127 128 return &URL{ 129 URL: url.URL{ 130 Path: s, 131 Fragment: fragment, 132 }, 133 Type: URLTypeLocal, 134 }, nil 135 } 136 137 // MustParse parses a "Git URL" and panics on failure 138 func MustParse(rawurl string) *URL { 139 u, err := Parse(rawurl) 140 if err != nil { 141 panic(err) 142 } 143 return u 144 } 145 146 // String returns a string representation of the URL 147 func (u URL) String() string { 148 var s string 149 switch u.Type { 150 case URLTypeURL: 151 return u.URL.String() 152 case URLTypeSCP: 153 if u.URL.User != nil { 154 s = u.URL.User.Username() + "@" 155 } 156 s += u.URL.Host + ":" + u.URL.Path 157 case URLTypeLocal: 158 s = u.URL.Path 159 } 160 if u.URL.RawQuery != "" { 161 s += "?" + u.URL.RawQuery 162 } 163 if u.URL.Fragment != "" { 164 s += "#" + u.URL.Fragment 165 } 166 return s 167 } 168 169 // StringNoFragment returns a string representation of the URL without its 170 // fragment 171 func (u URL) StringNoFragment() string { 172 u.URL.Fragment = "" 173 return u.String() 174 } 175 176 // IsLocal returns true if the Git URL refers to a local repository 177 func (u URL) IsLocal() bool { 178 return u.Type == URLTypeLocal || (u.Type == URLTypeURL && u.URL.Scheme == "file" && u.URL.Opaque == "") 179 } 180 181 // LocalPath returns the path to a local repository in OS-native format. It is 182 // assumed that IsLocal() is true 183 func (u URL) LocalPath() string { 184 switch { 185 case u.Type == URLTypeLocal: 186 return u.URL.Path 187 case u.Type == URLTypeURL && u.URL.Scheme == "file" && u.URL.Opaque == "": 188 if runtime.GOOS == "windows" && len(u.URL.Path) > 0 && u.URL.Path[0] == '/' { 189 return filepath.FromSlash(u.URL.Path[1:]) 190 } 191 return filepath.FromSlash(u.URL.Path) 192 } 193 panic("LocalPath called on non-local URL") 194 }