package git import ( "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/plumbing/transport/ssh" ) // AuthProvider handles authentication for Git operations type AuthProvider struct { cache map[string]transport.AuthMethod } // NewAuthProvider creates a new AuthProvider func NewAuthProvider() *AuthProvider { return &AuthProvider{ cache: make(map[string]transport.AuthMethod), } } // GetAuthMethod returns the appropriate authentication method for a URL func (ap *AuthProvider) GetAuthMethod(url string) (transport.AuthMethod, error) { // Check cache first if auth, ok := ap.cache[url]; ok { return auth, nil } var auth transport.AuthMethod var err error // Detect transport type from URL if strings.HasPrefix(url, "git@") || strings.HasPrefix(url, "ssh://") { // SSH authentication auth, err = ap.getSSHAuth() } else if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { // HTTPS authentication auth, err = ap.getHTTPSAuth(url) } else { return nil, fmt.Errorf("unsupported URL scheme: %s", url) } if err != nil { return nil, err } // Cache the auth method ap.cache[url] = auth return auth, nil } // getSSHAuth attempts to get SSH authentication func (ap *AuthProvider) getSSHAuth() (transport.AuthMethod, error) { // Try SSH agent first auth, err := ssh.NewSSHAgentAuth("git") if err == nil { return auth, nil } // Fallback to loading SSH keys from default locations homeDir, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("failed to get home directory: %w", err) } sshDir := filepath.Join(homeDir, ".ssh") // Try common key files keyFiles := []string{ "id_ed25519", "id_rsa", "id_ecdsa", "id_dsa", } for _, keyFile := range keyFiles { keyPath := filepath.Join(sshDir, keyFile) if _, err := os.Stat(keyPath); err == nil { // Try loading without passphrase first auth, err := ssh.NewPublicKeysFromFile("git", keyPath, "") if err == nil { return auth, nil } // If that fails, it might need a passphrase // For now, we'll skip passphrase-protected keys // In the future, we could prompt for the passphrase continue } } return nil, fmt.Errorf("no SSH authentication method available (tried ssh-agent and ~/.ssh keys)") } // getHTTPSAuth attempts to get HTTPS authentication func (ap *AuthProvider) getHTTPSAuth(url string) (transport.AuthMethod, error) { // Try git credential helper first auth, err := ap.tryGitCredentialHelper(url) if err == nil && auth != nil { return auth, nil } // Try environment variables username := os.Getenv("GIT_USERNAME") password := os.Getenv("GIT_PASSWORD") token := os.Getenv("GIT_TOKEN") if token != "" { // Use token as password (common for GitHub, GitLab, etc.) return &http.BasicAuth{ Username: "git", // Token usually goes in password field Password: token, }, nil } if username != "" && password != "" { return &http.BasicAuth{ Username: username, Password: password, }, nil } // No credentials available - return nil to let go-git try anonymous // (this will fail for private repos but that's expected) return nil, fmt.Errorf("no HTTPS credentials available (tried git credential helper and environment variables)") } // tryGitCredentialHelper attempts to use git's credential helper func (ap *AuthProvider) tryGitCredentialHelper(url string) (*http.BasicAuth, error) { // Build the credential request input := fmt.Sprintf("protocol=https\nhost=%s\n\n", extractHost(url)) // Call git credential fill cmd := exec.Command("git", "credential", "fill") cmd.Stdin = strings.NewReader(input) output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("git credential helper failed: %w", err) } // Parse the output lines := strings.Split(string(output), "\n") auth := &http.BasicAuth{} for _, line := range lines { parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) switch key { case "username": auth.Username = value case "password": auth.Password = value } } if auth.Username == "" || auth.Password == "" { return nil, fmt.Errorf("git credential helper did not return username and password") } return auth, nil } // extractHost extracts the host from a URL func extractHost(url string) string { // Remove protocol url = strings.TrimPrefix(url, "https://") url = strings.TrimPrefix(url, "http://") // Extract host (everything before the first /) parts := strings.SplitN(url, "/", 2) return parts[0] } // ClearCache clears the authentication cache func (ap *AuthProvider) ClearCache() { ap.cache = make(map[string]transport.AuthMethod) }