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  }