2016-12-19 18:45:22 +00:00
|
|
|
package lfsapi
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
2017-04-17 20:48:36 +00:00
|
|
|
"github.com/git-lfs/git-lfs/config"
|
2016-12-19 18:45:22 +00:00
|
|
|
"github.com/git-lfs/git-lfs/git"
|
2016-12-19 19:24:22 +00:00
|
|
|
"github.com/rubyist/tracerx"
|
2016-12-19 18:45:22 +00:00
|
|
|
)
|
|
|
|
|
2016-12-19 18:55:47 +00:00
|
|
|
type Access string
|
|
|
|
|
|
|
|
const (
|
2016-12-20 00:14:03 +00:00
|
|
|
NoneAccess Access = "none"
|
|
|
|
BasicAccess Access = "basic"
|
|
|
|
PrivateAccess Access = "private"
|
|
|
|
NegotiateAccess Access = "negotiate"
|
|
|
|
NTLMAccess Access = "ntlm"
|
|
|
|
emptyAccess Access = ""
|
|
|
|
defaultRemote = "origin"
|
2016-12-19 18:55:47 +00:00
|
|
|
)
|
2016-12-19 18:45:22 +00:00
|
|
|
|
|
|
|
type EndpointFinder interface {
|
|
|
|
NewEndpointFromCloneURL(rawurl string) Endpoint
|
|
|
|
NewEndpoint(rawurl string) Endpoint
|
|
|
|
Endpoint(operation, remote string) Endpoint
|
|
|
|
RemoteEndpoint(operation, remote string) Endpoint
|
|
|
|
GitRemoteURL(remote string, forpush bool) string
|
2016-12-19 18:55:47 +00:00
|
|
|
AccessFor(rawurl string) Access
|
2016-12-19 19:24:22 +00:00
|
|
|
SetAccess(rawurl string, access Access)
|
2016-12-19 18:45:22 +00:00
|
|
|
GitProtocol() string
|
|
|
|
}
|
|
|
|
|
|
|
|
type endpointGitFinder struct {
|
2017-10-25 22:05:08 +00:00
|
|
|
gitConfig *git.Configuration
|
|
|
|
gitEnv config.Environment
|
2016-12-19 18:45:22 +00:00
|
|
|
gitProtocol string
|
2016-12-19 19:24:22 +00:00
|
|
|
|
|
|
|
aliasMu sync.Mutex
|
|
|
|
aliases map[string]string
|
|
|
|
|
|
|
|
accessMu sync.Mutex
|
|
|
|
urlAccess map[string]Access
|
2017-04-17 20:48:36 +00:00
|
|
|
urlConfig *config.URLConfig
|
2016-12-19 18:45:22 +00:00
|
|
|
}
|
|
|
|
|
2017-10-25 22:05:08 +00:00
|
|
|
func NewEndpointFinder(ctx Context) EndpointFinder {
|
|
|
|
if ctx == nil {
|
|
|
|
ctx = NewContext(nil, nil, nil)
|
|
|
|
}
|
|
|
|
|
2016-12-19 18:45:22 +00:00
|
|
|
e := &endpointGitFinder{
|
2017-10-25 22:05:08 +00:00
|
|
|
gitConfig: ctx.GitConfig(),
|
|
|
|
gitEnv: ctx.GitEnv(),
|
2016-12-19 18:45:22 +00:00
|
|
|
gitProtocol: "https",
|
|
|
|
aliases: make(map[string]string),
|
2016-12-19 19:24:22 +00:00
|
|
|
urlAccess: make(map[string]Access),
|
2016-12-19 18:45:22 +00:00
|
|
|
}
|
|
|
|
|
2017-10-25 22:05:08 +00:00
|
|
|
e.urlConfig = config.NewURLConfig(e.gitEnv)
|
|
|
|
if v, ok := e.gitEnv.Get("lfs.gitprotocol"); ok {
|
|
|
|
e.gitProtocol = v
|
2016-12-19 18:45:22 +00:00
|
|
|
}
|
2017-10-25 22:05:08 +00:00
|
|
|
initAliases(e, e.gitEnv)
|
2016-12-19 18:45:22 +00:00
|
|
|
|
|
|
|
return e
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *endpointGitFinder) Endpoint(operation, remote string) Endpoint {
|
2017-01-25 15:59:31 +00:00
|
|
|
ep := e.getEndpoint(operation, remote)
|
|
|
|
ep.Operation = operation
|
|
|
|
return ep
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *endpointGitFinder) getEndpoint(operation, remote string) Endpoint {
|
2017-10-25 22:05:08 +00:00
|
|
|
if e.gitEnv == nil {
|
2016-12-19 18:45:22 +00:00
|
|
|
return Endpoint{}
|
|
|
|
}
|
|
|
|
|
|
|
|
if operation == "upload" {
|
2017-10-25 22:05:08 +00:00
|
|
|
if url, ok := e.gitEnv.Get("lfs.pushurl"); ok {
|
2016-12-19 18:45:22 +00:00
|
|
|
return e.NewEndpoint(url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-25 22:05:08 +00:00
|
|
|
if url, ok := e.gitEnv.Get("lfs.url"); ok {
|
2016-12-19 18:45:22 +00:00
|
|
|
return e.NewEndpoint(url)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(remote) > 0 && remote != defaultRemote {
|
|
|
|
if e := e.RemoteEndpoint(operation, remote); len(e.Url) > 0 {
|
|
|
|
return e
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return e.RemoteEndpoint(operation, defaultRemote)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *endpointGitFinder) RemoteEndpoint(operation, remote string) Endpoint {
|
2017-10-25 22:05:08 +00:00
|
|
|
if e.gitEnv == nil {
|
2016-12-19 18:45:22 +00:00
|
|
|
return Endpoint{}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(remote) == 0 {
|
|
|
|
remote = defaultRemote
|
|
|
|
}
|
|
|
|
|
|
|
|
// Support separate push URL if specified and pushing
|
|
|
|
if operation == "upload" {
|
2017-10-25 22:05:08 +00:00
|
|
|
if url, ok := e.gitEnv.Get("remote." + remote + ".lfspushurl"); ok {
|
2016-12-19 18:45:22 +00:00
|
|
|
return e.NewEndpoint(url)
|
|
|
|
}
|
|
|
|
}
|
2017-10-25 22:05:08 +00:00
|
|
|
if url, ok := e.gitEnv.Get("remote." + remote + ".lfsurl"); ok {
|
2016-12-19 18:45:22 +00:00
|
|
|
return e.NewEndpoint(url)
|
|
|
|
}
|
|
|
|
|
|
|
|
// finally fall back on git remote url (also supports pushurl)
|
|
|
|
if url := e.GitRemoteURL(remote, operation == "upload"); url != "" {
|
|
|
|
return e.NewEndpointFromCloneURL(url)
|
|
|
|
}
|
|
|
|
|
|
|
|
return Endpoint{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *endpointGitFinder) GitRemoteURL(remote string, forpush bool) string {
|
2017-10-25 22:05:08 +00:00
|
|
|
if e.gitEnv != nil {
|
2016-12-19 18:45:22 +00:00
|
|
|
if forpush {
|
2017-10-25 22:05:08 +00:00
|
|
|
if u, ok := e.gitEnv.Get("remote." + remote + ".pushurl"); ok {
|
2016-12-19 18:45:22 +00:00
|
|
|
return u
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-25 22:05:08 +00:00
|
|
|
if u, ok := e.gitEnv.Get("remote." + remote + ".url"); ok {
|
2016-12-19 18:45:22 +00:00
|
|
|
return u
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := git.ValidateRemote(remote); err == nil {
|
|
|
|
return remote
|
|
|
|
}
|
|
|
|
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *endpointGitFinder) NewEndpointFromCloneURL(rawurl string) Endpoint {
|
|
|
|
ep := e.NewEndpoint(rawurl)
|
|
|
|
if ep.Url == UrlUnknown {
|
|
|
|
return ep
|
|
|
|
}
|
|
|
|
|
|
|
|
if strings.HasSuffix(rawurl, "/") {
|
|
|
|
ep.Url = rawurl[0 : len(rawurl)-1]
|
|
|
|
}
|
|
|
|
|
|
|
|
// When using main remote URL for HTTP, append info/lfs
|
|
|
|
if path.Ext(ep.Url) == ".git" {
|
|
|
|
ep.Url += "/info/lfs"
|
|
|
|
} else {
|
|
|
|
ep.Url += ".git/info/lfs"
|
|
|
|
}
|
|
|
|
|
|
|
|
return ep
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *endpointGitFinder) NewEndpoint(rawurl string) Endpoint {
|
|
|
|
rawurl = e.ReplaceUrlAlias(rawurl)
|
2018-03-10 14:43:44 +00:00
|
|
|
if strings.HasPrefix(rawurl, "/") {
|
|
|
|
return endpointFromLocalPath(rawurl)
|
|
|
|
}
|
2016-12-19 18:45:22 +00:00
|
|
|
u, err := url.Parse(rawurl)
|
|
|
|
if err != nil {
|
|
|
|
return endpointFromBareSshUrl(rawurl)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch u.Scheme {
|
|
|
|
case "ssh":
|
|
|
|
return endpointFromSshUrl(u)
|
|
|
|
case "http", "https":
|
|
|
|
return endpointFromHttpUrl(u)
|
|
|
|
case "git":
|
|
|
|
return endpointFromGitUrl(u, e)
|
|
|
|
case "":
|
|
|
|
return endpointFromBareSshUrl(u.String())
|
|
|
|
default:
|
lfsapi: handle SSH hostnames and aliases without users
It's very common for a user to specify an SSH remote that refers to an
alias in their gitconfig file. For example, if a user must use
different SSH keys when pushing to different remotes, setting different
aliases that use those different keys is the simplest and easiest way to
accomplish that goal.
Unfortunately, our parsing logic was broken for SSH remotes that used
bare SSH URLs (that is, without “ssh://”) with a hostname or alias, but
without a username component. The URL parser we used saw the hostname
portion as the scheme of a URL, and we passed through any URLs with
unknown schemes. This led to a failure to push, since we misparsed the
URL as a non-SSH URL and gave up when it also didn't look like an
HTTP(S) URL.
If we get an unknown “scheme” when parsing a URL, check if the URL uses
a remote helper (that is, it contains a double colon), and pass it
through. Otherwise, do what Git does, and assume the URL is in fact an
SSH URL, and handle it accordingly.
We already handle the url.insteadof syntax properly, even if it looks
like a scheme. Add tests that we continue to do so, and for the new
code and all the various permutations mentioned above, and update the
existing integration test to reflect our new parsing strategy.
2018-09-05 21:23:12 +00:00
|
|
|
if strings.HasPrefix(rawurl, u.Scheme+"::") {
|
|
|
|
// Looks like a remote helper; just pass it through.
|
|
|
|
return Endpoint{Url: rawurl}
|
|
|
|
}
|
|
|
|
// We probably got here because the "scheme" that was parsed is
|
|
|
|
// a hostname (whether FQDN or single word) and the URL parser
|
|
|
|
// didn't know what to do with it. Do what Git does and treat
|
|
|
|
// it as an SSH URL. This ensures we handle SSH config aliases
|
|
|
|
// properly.
|
|
|
|
return endpointFromBareSshUrl(u.String())
|
2016-12-19 18:45:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-19 18:55:47 +00:00
|
|
|
func (e *endpointGitFinder) AccessFor(rawurl string) Access {
|
2017-10-25 22:05:08 +00:00
|
|
|
if e.gitEnv == nil {
|
2016-12-19 18:55:47 +00:00
|
|
|
return NoneAccess
|
|
|
|
}
|
|
|
|
|
2016-12-23 17:42:57 +00:00
|
|
|
accessurl := urlWithoutAuth(rawurl)
|
|
|
|
|
2016-12-19 19:24:22 +00:00
|
|
|
e.accessMu.Lock()
|
|
|
|
defer e.accessMu.Unlock()
|
|
|
|
|
2016-12-23 17:42:57 +00:00
|
|
|
if cached, ok := e.urlAccess[accessurl]; ok {
|
2016-12-19 19:24:22 +00:00
|
|
|
return cached
|
|
|
|
}
|
|
|
|
|
2017-04-17 20:49:32 +00:00
|
|
|
e.urlAccess[accessurl] = e.fetchGitAccess(accessurl)
|
2016-12-23 17:42:57 +00:00
|
|
|
return e.urlAccess[accessurl]
|
2016-12-19 19:24:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e *endpointGitFinder) SetAccess(rawurl string, access Access) {
|
2016-12-23 17:42:57 +00:00
|
|
|
accessurl := urlWithoutAuth(rawurl)
|
|
|
|
key := fmt.Sprintf("lfs.%s.access", accessurl)
|
2016-12-19 19:24:22 +00:00
|
|
|
tracerx.Printf("setting repository access to %s", access)
|
|
|
|
|
|
|
|
e.accessMu.Lock()
|
|
|
|
defer e.accessMu.Unlock()
|
|
|
|
|
|
|
|
switch access {
|
|
|
|
case emptyAccess, NoneAccess:
|
2017-10-26 01:20:35 +00:00
|
|
|
e.gitConfig.UnsetLocalKey(key)
|
2016-12-23 17:42:57 +00:00
|
|
|
e.urlAccess[accessurl] = NoneAccess
|
2016-12-19 19:24:22 +00:00
|
|
|
default:
|
2017-10-26 01:20:35 +00:00
|
|
|
e.gitConfig.SetLocal(key, string(access))
|
2016-12-23 17:42:57 +00:00
|
|
|
e.urlAccess[accessurl] = access
|
2016-12-19 19:24:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-23 17:42:57 +00:00
|
|
|
func urlWithoutAuth(rawurl string) string {
|
|
|
|
if !strings.Contains(rawurl, "@") {
|
|
|
|
return rawurl
|
|
|
|
}
|
|
|
|
|
|
|
|
u, err := url.Parse(rawurl)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "Error parsing URL %q: %s", rawurl, err)
|
|
|
|
return rawurl
|
|
|
|
}
|
|
|
|
|
|
|
|
u.User = nil
|
|
|
|
return u.String()
|
|
|
|
}
|
|
|
|
|
2017-04-17 20:49:32 +00:00
|
|
|
func (e *endpointGitFinder) fetchGitAccess(rawurl string) Access {
|
|
|
|
if v, _ := e.urlConfig.Get("lfs", rawurl, "access"); len(v) > 0 {
|
2016-12-19 19:24:22 +00:00
|
|
|
access := Access(strings.ToLower(v))
|
|
|
|
if access == PrivateAccess {
|
2016-12-19 18:55:47 +00:00
|
|
|
return BasicAccess
|
|
|
|
}
|
2016-12-19 19:24:22 +00:00
|
|
|
return access
|
2016-12-19 18:55:47 +00:00
|
|
|
}
|
|
|
|
return NoneAccess
|
|
|
|
}
|
|
|
|
|
2016-12-19 18:45:22 +00:00
|
|
|
func (e *endpointGitFinder) GitProtocol() string {
|
|
|
|
return e.gitProtocol
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReplaceUrlAlias returns a url with a prefix from a `url.*.insteadof` git
|
|
|
|
// config setting. If multiple aliases match, use the longest one.
|
|
|
|
// See https://git-scm.com/docs/git-config for Git's docs.
|
|
|
|
func (e *endpointGitFinder) ReplaceUrlAlias(rawurl string) string {
|
|
|
|
e.aliasMu.Lock()
|
|
|
|
defer e.aliasMu.Unlock()
|
|
|
|
|
|
|
|
var longestalias string
|
|
|
|
for alias, _ := range e.aliases {
|
|
|
|
if !strings.HasPrefix(rawurl, alias) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if longestalias < alias {
|
|
|
|
longestalias = alias
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(longestalias) > 0 {
|
|
|
|
return e.aliases[longestalias] + rawurl[len(longestalias):]
|
|
|
|
}
|
|
|
|
|
|
|
|
return rawurl
|
|
|
|
}
|
|
|
|
|
2017-10-25 21:33:20 +00:00
|
|
|
func initAliases(e *endpointGitFinder, git config.Environment) {
|
2016-12-19 18:45:22 +00:00
|
|
|
prefix := "url."
|
|
|
|
suffix := ".insteadof"
|
|
|
|
for gitkey, gitval := range git.All() {
|
2017-04-12 21:29:11 +00:00
|
|
|
if len(gitval) == 0 || !(strings.HasPrefix(gitkey, prefix) && strings.HasSuffix(gitkey, suffix)) {
|
2016-12-19 18:45:22 +00:00
|
|
|
continue
|
|
|
|
}
|
2017-04-14 17:29:52 +00:00
|
|
|
if _, ok := e.aliases[gitval[len(gitval)-1]]; ok {
|
2016-12-19 18:45:22 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "WARNING: Multiple 'url.*.insteadof' keys with the same alias: %q\n", gitval)
|
|
|
|
}
|
2017-04-14 17:29:52 +00:00
|
|
|
e.aliases[gitval[len(gitval)-1]] = gitkey[len(prefix) : len(gitkey)-len(suffix)]
|
2016-12-19 18:45:22 +00:00
|
|
|
}
|
|
|
|
}
|