github.com/git-lfs/git-lfs@v2.5.2+incompatible/lfsapi/endpoint_finder.go (about) 1 package lfsapi 2 3 import ( 4 "fmt" 5 "net/url" 6 "os" 7 "path" 8 "strings" 9 "sync" 10 11 "github.com/git-lfs/git-lfs/config" 12 "github.com/git-lfs/git-lfs/git" 13 "github.com/rubyist/tracerx" 14 ) 15 16 type Access string 17 18 const ( 19 NoneAccess Access = "none" 20 BasicAccess Access = "basic" 21 PrivateAccess Access = "private" 22 NegotiateAccess Access = "negotiate" 23 NTLMAccess Access = "ntlm" 24 emptyAccess Access = "" 25 defaultRemote = "origin" 26 ) 27 28 type EndpointFinder interface { 29 NewEndpointFromCloneURL(rawurl string) Endpoint 30 NewEndpoint(rawurl string) Endpoint 31 Endpoint(operation, remote string) Endpoint 32 RemoteEndpoint(operation, remote string) Endpoint 33 GitRemoteURL(remote string, forpush bool) string 34 AccessFor(rawurl string) Access 35 SetAccess(rawurl string, access Access) 36 GitProtocol() string 37 } 38 39 type endpointGitFinder struct { 40 gitConfig *git.Configuration 41 gitEnv config.Environment 42 gitProtocol string 43 44 aliasMu sync.Mutex 45 aliases map[string]string 46 47 accessMu sync.Mutex 48 urlAccess map[string]Access 49 urlConfig *config.URLConfig 50 } 51 52 func NewEndpointFinder(ctx Context) EndpointFinder { 53 if ctx == nil { 54 ctx = NewContext(nil, nil, nil) 55 } 56 57 e := &endpointGitFinder{ 58 gitConfig: ctx.GitConfig(), 59 gitEnv: ctx.GitEnv(), 60 gitProtocol: "https", 61 aliases: make(map[string]string), 62 urlAccess: make(map[string]Access), 63 } 64 65 e.urlConfig = config.NewURLConfig(e.gitEnv) 66 if v, ok := e.gitEnv.Get("lfs.gitprotocol"); ok { 67 e.gitProtocol = v 68 } 69 initAliases(e, e.gitEnv) 70 71 return e 72 } 73 74 func (e *endpointGitFinder) Endpoint(operation, remote string) Endpoint { 75 ep := e.getEndpoint(operation, remote) 76 ep.Operation = operation 77 return ep 78 } 79 80 func (e *endpointGitFinder) getEndpoint(operation, remote string) Endpoint { 81 if e.gitEnv == nil { 82 return Endpoint{} 83 } 84 85 if operation == "upload" { 86 if url, ok := e.gitEnv.Get("lfs.pushurl"); ok { 87 return e.NewEndpoint(url) 88 } 89 } 90 91 if url, ok := e.gitEnv.Get("lfs.url"); ok { 92 return e.NewEndpoint(url) 93 } 94 95 if len(remote) > 0 && remote != defaultRemote { 96 if e := e.RemoteEndpoint(operation, remote); len(e.Url) > 0 { 97 return e 98 } 99 } 100 101 return e.RemoteEndpoint(operation, defaultRemote) 102 } 103 104 func (e *endpointGitFinder) RemoteEndpoint(operation, remote string) Endpoint { 105 if e.gitEnv == nil { 106 return Endpoint{} 107 } 108 109 if len(remote) == 0 { 110 remote = defaultRemote 111 } 112 113 // Support separate push URL if specified and pushing 114 if operation == "upload" { 115 if url, ok := e.gitEnv.Get("remote." + remote + ".lfspushurl"); ok { 116 return e.NewEndpoint(url) 117 } 118 } 119 if url, ok := e.gitEnv.Get("remote." + remote + ".lfsurl"); ok { 120 return e.NewEndpoint(url) 121 } 122 123 // finally fall back on git remote url (also supports pushurl) 124 if url := e.GitRemoteURL(remote, operation == "upload"); url != "" { 125 return e.NewEndpointFromCloneURL(url) 126 } 127 128 return Endpoint{} 129 } 130 131 func (e *endpointGitFinder) GitRemoteURL(remote string, forpush bool) string { 132 if e.gitEnv != nil { 133 if forpush { 134 if u, ok := e.gitEnv.Get("remote." + remote + ".pushurl"); ok { 135 return u 136 } 137 } 138 139 if u, ok := e.gitEnv.Get("remote." + remote + ".url"); ok { 140 return u 141 } 142 } 143 144 if err := git.ValidateRemote(remote); err == nil { 145 return remote 146 } 147 148 return "" 149 } 150 151 func (e *endpointGitFinder) NewEndpointFromCloneURL(rawurl string) Endpoint { 152 ep := e.NewEndpoint(rawurl) 153 if ep.Url == UrlUnknown { 154 return ep 155 } 156 157 if strings.HasSuffix(rawurl, "/") { 158 ep.Url = rawurl[0 : len(rawurl)-1] 159 } 160 161 // When using main remote URL for HTTP, append info/lfs 162 if path.Ext(ep.Url) == ".git" { 163 ep.Url += "/info/lfs" 164 } else { 165 ep.Url += ".git/info/lfs" 166 } 167 168 return ep 169 } 170 171 func (e *endpointGitFinder) NewEndpoint(rawurl string) Endpoint { 172 rawurl = e.ReplaceUrlAlias(rawurl) 173 if strings.HasPrefix(rawurl, "/") { 174 return endpointFromLocalPath(rawurl) 175 } 176 u, err := url.Parse(rawurl) 177 if err != nil { 178 return endpointFromBareSshUrl(rawurl) 179 } 180 181 switch u.Scheme { 182 case "ssh": 183 return endpointFromSshUrl(u) 184 case "http", "https": 185 return endpointFromHttpUrl(u) 186 case "git": 187 return endpointFromGitUrl(u, e) 188 case "": 189 return endpointFromBareSshUrl(u.String()) 190 default: 191 // Just passthrough to preserve 192 return Endpoint{Url: rawurl} 193 } 194 } 195 196 func (e *endpointGitFinder) AccessFor(rawurl string) Access { 197 if e.gitEnv == nil { 198 return NoneAccess 199 } 200 201 accessurl := urlWithoutAuth(rawurl) 202 203 e.accessMu.Lock() 204 defer e.accessMu.Unlock() 205 206 if cached, ok := e.urlAccess[accessurl]; ok { 207 return cached 208 } 209 210 e.urlAccess[accessurl] = e.fetchGitAccess(accessurl) 211 return e.urlAccess[accessurl] 212 } 213 214 func (e *endpointGitFinder) SetAccess(rawurl string, access Access) { 215 accessurl := urlWithoutAuth(rawurl) 216 key := fmt.Sprintf("lfs.%s.access", accessurl) 217 tracerx.Printf("setting repository access to %s", access) 218 219 e.accessMu.Lock() 220 defer e.accessMu.Unlock() 221 222 switch access { 223 case emptyAccess, NoneAccess: 224 e.gitConfig.UnsetLocalKey(key) 225 e.urlAccess[accessurl] = NoneAccess 226 default: 227 e.gitConfig.SetLocal(key, string(access)) 228 e.urlAccess[accessurl] = access 229 } 230 } 231 232 func urlWithoutAuth(rawurl string) string { 233 if !strings.Contains(rawurl, "@") { 234 return rawurl 235 } 236 237 u, err := url.Parse(rawurl) 238 if err != nil { 239 fmt.Fprintf(os.Stderr, "Error parsing URL %q: %s", rawurl, err) 240 return rawurl 241 } 242 243 u.User = nil 244 return u.String() 245 } 246 247 func (e *endpointGitFinder) fetchGitAccess(rawurl string) Access { 248 if v, _ := e.urlConfig.Get("lfs", rawurl, "access"); len(v) > 0 { 249 access := Access(strings.ToLower(v)) 250 if access == PrivateAccess { 251 return BasicAccess 252 } 253 return access 254 } 255 return NoneAccess 256 } 257 258 func (e *endpointGitFinder) GitProtocol() string { 259 return e.gitProtocol 260 } 261 262 // ReplaceUrlAlias returns a url with a prefix from a `url.*.insteadof` git 263 // config setting. If multiple aliases match, use the longest one. 264 // See https://git-scm.com/docs/git-config for Git's docs. 265 func (e *endpointGitFinder) ReplaceUrlAlias(rawurl string) string { 266 e.aliasMu.Lock() 267 defer e.aliasMu.Unlock() 268 269 var longestalias string 270 for alias, _ := range e.aliases { 271 if !strings.HasPrefix(rawurl, alias) { 272 continue 273 } 274 275 if longestalias < alias { 276 longestalias = alias 277 } 278 } 279 280 if len(longestalias) > 0 { 281 return e.aliases[longestalias] + rawurl[len(longestalias):] 282 } 283 284 return rawurl 285 } 286 287 func initAliases(e *endpointGitFinder, git config.Environment) { 288 prefix := "url." 289 suffix := ".insteadof" 290 for gitkey, gitval := range git.All() { 291 if len(gitval) == 0 || !(strings.HasPrefix(gitkey, prefix) && strings.HasSuffix(gitkey, suffix)) { 292 continue 293 } 294 if _, ok := e.aliases[gitval[len(gitval)-1]]; ok { 295 fmt.Fprintf(os.Stderr, "WARNING: Multiple 'url.*.insteadof' keys with the same alias: %q\n", gitval) 296 } 297 e.aliases[gitval[len(gitval)-1]] = gitkey[len(prefix) : len(gitkey)-len(suffix)] 298 } 299 }