Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 86 additions & 5 deletions tibok/tibok/Services/LibGit2Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<OpaquePointer?>?,
username: UnsafePointer<CChar>?
) -> 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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
45 changes: 43 additions & 2 deletions tibok/user_docs/features/git-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down