diff --git a/tibok/tibok/Services/LibGit2Service.swift b/tibok/tibok/Services/LibGit2Service.swift index ff83296..c8c0c69 100644 --- a/tibok/tibok/Services/LibGit2Service.swift +++ b/tibok/tibok/Services/LibGit2Service.swift @@ -38,12 +38,12 @@ struct LibGit2Error: Error, LocalizedError { // Authentication errors if lowercased.contains("authentication") || lowercased.contains("credentials") { - return "Authentication failed. Check your SSH keys are added to ssh-agent." + return "Authentication failed. Ensure SSH keys exist in ~/.ssh/ directory (without passphrase) or are added to ssh-agent." } // Permission errors if lowercased.contains("permission denied") || lowercased.contains("publickey") { - return "Permission denied. Ensure your SSH key is configured correctly." + return "Permission denied. Ensure your SSH key is configured correctly and added to your Git hosting service." } // Network errors @@ -785,6 +785,86 @@ class LibGit2Service: ObservableObject { // MARK: - Remote Operations + /// Helper function to create SSH credentials with fallback + /// Tries SSH agent first, then falls back to reading SSH keys from ~/.ssh directory + /// Note: Direct key loading only works with unencrypted SSH keys + private static func createSSHCredentials( + out: UnsafeMutablePointer?, + username: UnsafePointer? + ) -> Int32 { + // Try SSH agent first (current behavior) + let agentResult = git_credential_ssh_key_from_agent(out, username) + if agentResult == 0 { + #if DEBUG + print("[LibGit2] SSH authentication: using key from ssh-agent") + #endif + return agentResult + } + + #if DEBUG + print("[LibGit2] SSH agent auth failed, trying direct key files from ~/.ssh") + #endif + + // SSH agent failed, try loading keys directly from ~/.ssh + let homeDir = FileManager.default.homeDirectoryForCurrentUser.path + let sshDir = "\(homeDir)/.ssh" + + // Common SSH key filenames in order of preference + let keyFiles = [ + "id_ed25519", // Ed25519 (modern, recommended) + "id_rsa", // RSA (most common) + "id_ecdsa", // ECDSA + "id_dsa" // DSA (legacy) + ] + + for keyFile in keyFiles { + let privateKeyPath = "\(sshDir)/\(keyFile)" + let publicKeyPath = "\(sshDir)/\(keyFile).pub" + + // Check if both private and public key files exist + if FileManager.default.fileExists(atPath: privateKeyPath) && + FileManager.default.fileExists(atPath: publicKeyPath) { + + #if DEBUG + print("[LibGit2] Found SSH key pair: \(keyFile)") + #endif + + // Try to create credential with this key pair + // Note: Empty passphrase only works with unencrypted keys + // Passphrase-protected keys must be added to ssh-agent + let result = privateKeyPath.withCString { privateKey in + publicKeyPath.withCString { publicKey in + git_credential_ssh_key_new( + out, + username, + publicKey, + privateKey, + "" // Empty passphrase - only works for unencrypted keys + ) + } + } + + if result == 0 { + #if DEBUG + print("[LibGit2] Successfully authenticated with \(keyFile)") + #endif + return result + } else { + #if DEBUG + print("[LibGit2] Failed to authenticate with \(keyFile) (may be encrypted)") + #endif + } + } + } + + #if DEBUG + print("[LibGit2] All SSH authentication methods failed") + #endif + + // All methods failed + return GIT_PASSTHROUGH.rawValue + } + /// Push to remote func push(in repoURL: URL) -> (success: Bool, error: String?, alreadyUpToDate: Bool) { do { @@ -820,9 +900,9 @@ class LibGit2Service: ObservableObject { // Set up credentials callback for SSH opts.callbacks.credentials = { (out, url, username_from_url, allowed_types, payload) -> Int32 in - // Try SSH agent first + // Try SSH authentication (agent first, then direct key files) if allowed_types & GIT_CREDENTIAL_SSH_KEY.rawValue != 0 { - return git_credential_ssh_key_from_agent(out, username_from_url) + return LibGit2Service.createSSHCredentials(out: out, username: username_from_url) } return GIT_PASSTHROUGH.rawValue } @@ -881,8 +961,9 @@ class LibGit2Service: ObservableObject { // Set up credentials callback opts.callbacks.credentials = { (out, url, username_from_url, allowed_types, payload) -> Int32 in + // Try SSH authentication (agent first, then direct key files) if allowed_types & GIT_CREDENTIAL_SSH_KEY.rawValue != 0 { - return git_credential_ssh_key_from_agent(out, username_from_url) + return LibGit2Service.createSSHCredentials(out: out, username: username_from_url) } return GIT_PASSTHROUGH.rawValue } diff --git a/tibok/user_docs/features/git-integration.md b/tibok/user_docs/features/git-integration.md index 17ed578..85dbfb7 100644 --- a/tibok/user_docs/features/git-integration.md +++ b/tibok/user_docs/features/git-integration.md @@ -170,18 +170,59 @@ The Git menu provides access to all Git operations: ## How It Works -tibok uses your system's Git installation (`/usr/bin/git`) to execute commands. This means: +tibok uses libgit2 for native Git operations. This means: - **Authentication**: Uses your existing SSH keys and credentials - **Configuration**: Respects your `.gitconfig` settings - **Compatibility**: Works with any Git remote (GitHub, GitLab, Bitbucket, etc.) +### SSH Authentication + +For SSH remotes (git@github.com:user/repo.git), tibok supports multiple authentication methods: + +1. **SSH Agent** - First tries to use keys from ssh-agent (supports encrypted keys) +2. **Direct Keys** - Falls back to reading unencrypted keys from `~/.ssh/` directory + +Supported key types (in order of preference): +- `~/.ssh/id_ed25519` (Ed25519 - recommended) +- `~/.ssh/id_rsa` (RSA - most common) +- `~/.ssh/id_ecdsa` (ECDSA) +- `~/.ssh/id_dsa` (DSA - legacy) + +**Important**: +- **Passphrase-protected keys MUST be added to ssh-agent** for authentication to work +- Unencrypted keys (no passphrase) work automatically if present in `~/.ssh/` +- To add an encrypted key to ssh-agent: `ssh-add ~/.ssh/id_rsa` + If `git push` works in Terminal, it will work in tibok. ## Requirements - Git must be installed on your system (included with Xcode Command Line Tools) -- For push/pull: Git credentials configured (SSH keys or credential helper) +- For push/pull with SSH: SSH keys configured in `~/.ssh/` directory +- For push/pull with HTTPS: Git credential helper configured + +## Troubleshooting + +### Push/Pull Fails with Authentication Error + +**Error**: "Authentication failed. Ensure SSH keys exist in ~/.ssh/ directory (without passphrase) or are added to ssh-agent." + +**Solutions**: +1. **For unencrypted keys**: Ensure your SSH key exists in `~/.ssh/` directory with matching `.pub` file +2. **For passphrase-protected keys**: Add to ssh-agent with `ssh-add ~/.ssh/id_rsa` +3. **Test SSH connection**: `ssh -T git@github.com` (should authenticate successfully) +4. **Verify remote URL**: Check it uses SSH format with `git remote -v` in Terminal +5. **Check key permissions**: Private keys should have 600 permissions (`chmod 600 ~/.ssh/id_rsa`) + +**Note**: Direct key loading (method 2) only works with unencrypted SSH keys. If your key has a passphrase, you must use ssh-agent (method 1). + +### Permission Denied (publickey) + +This means your SSH key is not authorized on the remote server: +1. Add your public key to GitHub/GitLab/Bitbucket account settings +2. Copy public key: `cat ~/.ssh/id_rsa.pub` +3. Add to your account under Settings → SSH Keys ## Tips