diff --git a/src/auth/git_auth.rs b/src/auth/git_auth.rs new file mode 100644 index 0000000..648788c --- /dev/null +++ b/src/auth/git_auth.rs @@ -0,0 +1,33 @@ +use git2::{CertificateCheckStatus, CredentialType, RemoteCallbacks}; +use log::debug; +use std::sync::{Arc, Mutex}; + +use crate::auth::{git_http::try_userpass_authentication, git_ssh::ssh_authenticate_git}; + +pub fn setup_auth_callbacks() -> RemoteCallbacks<'static> { + let mut callbacks = RemoteCallbacks::new(); + + // Track attempt count across callback invocations + let attempt_count: Arc> = Arc::new(Mutex::new(0)); + + callbacks.credentials(move |url, username_from_url, allowed_types| { + let mut count = attempt_count.lock().unwrap(); + *count += 1; + let current_attempt = *count; + drop(count); + + if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { + try_userpass_authentication(username_from_url) + } else { + ssh_authenticate_git(url, username_from_url, allowed_types, current_attempt) + } + }); + + // Set up certificate check callback for HTTPS + callbacks.certificate_check(|_cert, _host| { + debug!("Skipping certificate verification (INSECURE)"); + Ok(CertificateCheckStatus::CertificateOk) + }); + + callbacks +} diff --git a/src/auth/git_http.rs b/src/auth/git_http.rs new file mode 100644 index 0000000..d672a3a --- /dev/null +++ b/src/auth/git_http.rs @@ -0,0 +1,55 @@ +use dialoguer::{Input, Password, theme::ColorfulTheme}; +use git2::{Cred, Error, ErrorClass, ErrorCode}; +use log::debug; + +pub fn try_userpass_authentication(username_from_url: Option<&str>) -> Result { + debug!("USER_PASS_PLAINTEXT authentication is allowed, prompting for credentials"); + + // Prompt for username if not provided in URL + let username = if let Some(user) = username_from_url { + user.to_string() + } else { + Input::::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your username") + .interact() + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to read username: {e}"), + ) + })? + }; + + let token = Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your personal access token") + .interact() + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to read token: {e}"), + ) + })?; + + if !username.is_empty() && !token.is_empty() { + debug!("Creating credentials with username and token"); + match Cred::userpass_plaintext(&username, &token) { + Ok(cred) => { + debug!("Username/token authentication succeeded"); + Ok(cred) + } + Err(e) => { + debug!("Username/token authentication failed: {e}"); + Err(e) + } + } + } else { + debug!("Username or token is empty, skipping userpass authentication"); + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "Username or token cannot be empty", + )) + } +} diff --git a/src/auth/git_ssh.rs b/src/auth/git_ssh.rs new file mode 100644 index 0000000..166c362 --- /dev/null +++ b/src/auth/git_ssh.rs @@ -0,0 +1,250 @@ +use git2::{Cred, CredentialType, Error, ErrorClass, ErrorCode}; +use log::debug; +use std::path::Path; +use std::process::{Command, Stdio}; + +use crate::auth::ssh_utils::{add_key_interactive, parse_ssh_agent_output}; + +pub fn ssh_authenticate_git( + url: &str, + username_from_url: Option<&str>, + allowed_types: CredentialType, + attempt_count: usize, +) -> Result { + debug!("Git authentication attempt #{attempt_count} for URL: {url}"); + debug!("Username from URL: {username_from_url:?}"); + debug!("Allowed credential types: {allowed_types:?}"); + + // Prevent infinite loops + if attempt_count > 3 { + debug!( + "Too many authentication attempts ({attempt_count}), failing to prevent infinite loop" + ); + return Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "Too many authentication attempts", + )); + } + + if allowed_types.contains(CredentialType::SSH_KEY) { + if let Some(username) = username_from_url { + debug!("SSH key authentication is allowed, trying SSH agent"); + + // handling the case where ssh-agent is running but empty + if attempt_count == 2 { + debug!("Second attempt: trying to add SSH keys to agent before authentication"); + if std::env::var("SSH_AUTH_SOCK").is_ok() { + if let Err(e) = add_all_ssh_keys() { + debug!("Failed to add keys to ssh-agent on second attempt: {e}"); + } else { + debug!("Keys added to ssh-agent, proceeding with authentication"); + } + } + } + + if let Ok(cred) = try_ssh_agent_auth(username) { + return Ok(cred); + } + } else { + debug!("No username provided for SSH authentication"); + } + } + + debug!("All authentication methods failed for attempt {attempt_count}"); + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Authentication failed - attempt {attempt_count}"), + )) +} + +fn try_ssh_agent_auth(username: &str) -> Result { + debug!("Attempting SSH agent authentication for user: {username}"); + + if std::env::var("SSH_AUTH_SOCK").is_err() { + debug!("SSH_AUTH_SOCK not set, attempting to spawn ssh-agent and add keys"); + spawn_ssh_agent_and_add_keys()?; + } + + match Cred::ssh_key_from_agent(username) { + Ok(cred) => { + debug!("SSH agent authentication succeeded"); + + Ok(cred) + } + Err(e) => { + debug!("SSH agent authentication failed: {e}"); + + // Fallback to trying SSH key files directly + debug!("Falling back to direct SSH key file authentication"); + try_ssh_key_files_directly(username) + } + } +} + +fn spawn_ssh_agent_and_add_keys() -> Result<(), Error> { + debug!("SSH_AUTH_SOCK not set, spawning ssh-agent"); + + let output = Command::new("ssh-agent").arg("-s").output().map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to spawn ssh-agent: {e}"), + ) + })?; + + if !output.status.success() { + return Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("ssh-agent failed with status: {}", output.status), + )); + } + + let agent_output = String::from_utf8_lossy(&output.stdout); + debug!("ssh-agent output: {agent_output}"); + + let env_vars = parse_ssh_agent_output(&agent_output); + + for (key, value) in &env_vars { + unsafe { + std::env::set_var(key, value); + } + debug!("Set environment variable: {key}={value}"); + } + + if !env_vars.contains_key("SSH_AUTH_SOCK") { + return Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "Failed to parse SSH_AUTH_SOCK from ssh-agent output", + )); + } + + add_all_ssh_keys()?; + + Ok(()) +} + +fn add_all_ssh_keys() -> Result<(), Error> { + debug!("Adding all SSH keys from .ssh folder to ssh-agent"); + + let home_dir = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .unwrap_or_else(|_| ".".to_string()); + + let ssh_dir = Path::new(&home_dir).join(".ssh"); + + if !ssh_dir.exists() { + debug!("SSH directory {ssh_dir:?} does not exist"); + return Ok(()); + } + + let key_files = ["id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"]; + let mut added_count = 0; + + for key_name in &key_files { + let key_path = ssh_dir.join(key_name); + + if key_path.exists() { + debug!("Found SSH key: {key_path:?}"); + + // First try a quick non-interactive add (for keys without passphrase) + let quick_result = Command::new("ssh-add") + .arg(&key_path) + .env( + "SSH_AUTH_SOCK", + std::env::var("SSH_AUTH_SOCK").unwrap_or_default(), + ) + .stdin(Stdio::null()) // No input for quick try + .stdout(Stdio::null()) // Suppress output for quick try + .stderr(Stdio::piped()) // Capture errors to check if passphrase is needed + .output(); + + match quick_result { + Ok(output) if output.status.success() => { + debug!("Successfully added key without interaction: {key_name}"); + added_count += 1; + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + debug!("Quick add failed for {key_name}: {stderr}"); + + debug!("Key {key_name} appears to need passphrase, trying interactive add"); + + match add_key_interactive(&key_path, key_name) { + Ok(true) => { + debug!("Successfully added key interactively: {key_name}"); + added_count += 1; + } + Ok(false) => { + debug!("User skipped key: {key_name}"); + } + Err(e) => { + debug!("Interactive add failed for {key_name}: {e}"); + } + } + } + Err(e) => { + debug!("Error running ssh-add for {key_name}: {e}"); + } + } + } else { + debug!("SSH key not found: {key_path:?}"); + } + } + + debug!("Added {added_count} SSH keys to ssh-agent"); + + if added_count == 0 { + debug!("No SSH keys were added"); + println!("No SSH keys were added to ssh-agent."); + println!("You may need to generate SSH keys or check your ~/.ssh directory."); + } else { + println!("Successfully added {added_count} SSH key(s) to ssh-agent."); + } + + Ok(()) +} + +fn try_ssh_key_files_directly(username: &str) -> Result { + debug!("Trying SSH key files directly for user: {username}"); + + let home_dir = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .unwrap_or_else(|_| ".".to_string()); + + let ssh_dir = Path::new(&home_dir).join(".ssh"); + let key_files = ["id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"]; + + for key_name in &key_files { + let private_key_path = ssh_dir.join(key_name); + let public_key_path = ssh_dir.join(format!("{key_name}.pub")); + + if private_key_path.exists() && public_key_path.exists() { + debug!("Trying SSH key pair: {key_name} / {key_name}.pub"); + + match Cred::ssh_key( + username, + Some(&public_key_path), + &private_key_path, + None, // No passphrase for now + ) { + Ok(cred) => { + debug!("SSH key authentication succeeded with {key_name}"); + return Ok(cred); + } + Err(e) => { + debug!("SSH key authentication failed with {key_name}: {e}"); + } + } + } + } + + Err(Error::new( + ErrorCode::Auth, + ErrorClass::Net, + "No valid SSH key pairs found or all failed authentication", + )) +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..6ec4ab9 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,4 @@ +pub mod git_auth; +mod git_http; +mod git_ssh; +mod ssh_utils; diff --git a/src/auth/ssh_utils.rs b/src/auth/ssh_utils.rs new file mode 100644 index 0000000..4c95ae9 --- /dev/null +++ b/src/auth/ssh_utils.rs @@ -0,0 +1,82 @@ +use std::{ + collections::HashMap, + path::Path, + process::{Command, Stdio}, +}; + +use dialoguer::{Confirm, theme::ColorfulTheme}; +use git2::{Error, ErrorClass, ErrorCode}; +use log::debug; + +pub fn parse_ssh_agent_output(output: &str) -> HashMap { + let mut env_vars = HashMap::new(); + + for line in output.lines() { + if line.contains('=') && (line.contains("SSH_AUTH_SOCK") || line.contains("SSH_AGENT_PID")) + { + if let Some(var_part) = line.split(';').next() { + if let Some((key, value)) = var_part.split_once('=') { + env_vars.insert(key.to_string(), value.to_string()); + } + } + } + } + + env_vars +} + +pub fn add_key_interactive(key_path: &Path, key_name: &str) -> Result { + debug!("Trying interactive ssh-add for key: {key_name}"); + + // Ask user if they want to add this key interactively + let should_add = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "Add SSH key '{key_name}' to ssh-agent? (you may be prompted for passphrase)" + )) + .default(true) + .interact() + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to get user confirmation: {e}"), + ) + })?; + + if !should_add { + debug!("User chose not to add key: {key_name}"); + return Ok(false); + } + + println!("Adding SSH key: {key_name}"); + println!("If the key is passphrase-protected, you will be prompted to enter it."); + + // Use interactive ssh-add - this will prompt the user directly in the terminal + let status = Command::new("ssh-add") + .arg(key_path) + .env( + "SSH_AUTH_SOCK", + std::env::var("SSH_AUTH_SOCK").unwrap_or_default(), + ) + .stdin(Stdio::inherit()) // Allow user to input passphrase directly + .stdout(Stdio::inherit()) // Show ssh-add output to user + .stderr(Stdio::inherit()) // Show ssh-add errors to user + .status() // Use status() instead of output() to allow real-time interaction + .map_err(|e| { + Error::new( + ErrorCode::Auth, + ErrorClass::Net, + format!("Failed to spawn ssh-add: {e}"), + ) + })?; + + if status.success() { + debug!("Successfully added key: {key_name}"); + println!("✓ SSH key '{key_name}' added successfully!"); + Ok(true) + } else { + debug!("Interactive ssh-add failed for key: {key_name}"); + println!("✗ Failed to add SSH key '{key_name}'"); + Ok(false) + } +} diff --git a/src/events/git_clone.rs b/src/events/git_clone.rs index 37ef48d..b582d6d 100644 --- a/src/events/git_clone.rs +++ b/src/events/git_clone.rs @@ -1,7 +1,7 @@ use super::AtomicEvent; -use crate::bgit_error::BGitError; +use crate::auth::git_auth::setup_auth_callbacks; +use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; -use git2::{Cred, CredentialType}; use std::env; use std::path::Path; @@ -87,142 +87,10 @@ impl GitClone { } } - /// Set up authentication callbacks for git operations - fn setup_auth_callbacks() -> git2::RemoteCallbacks<'static> { - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - let mut callbacks = git2::RemoteCallbacks::new(); - let attempt_count = Arc::new(AtomicUsize::new(0)); - - callbacks.credentials(move |url, username_from_url, allowed_types| { - let current_attempt = attempt_count.fetch_add(1, Ordering::SeqCst); - // Limit authentication attempts to prevent infinite loops - if current_attempt > 3 { - return Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - "Maximum authentication attempts exceeded", - )); - } - - // If SSH key authentication is allowed - if allowed_types.contains(CredentialType::SSH_KEY) { - if let Some(username) = username_from_url { - // Try SSH agent first (most common and secure) - match Cred::ssh_key_from_agent(username) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH agent failed: {e}"); - } - } - - // Try to find SSH keys in standard locations - let home_dir = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .unwrap_or_else(|_| ".".to_string()); - - let ssh_dir = Path::new(&home_dir).join(".ssh"); - - // Common SSH key file names in order of preference - let key_files = [ - ("id_ed25519", "id_ed25519.pub"), - ("id_rsa", "id_rsa.pub"), - ("id_ecdsa", "id_ecdsa.pub"), - ("id_dsa", "id_dsa.pub"), - ]; - - for (private_name, public_name) in &key_files { - let private_key = ssh_dir.join(private_name); - let public_key = ssh_dir.join(public_name); - - if private_key.exists() { - // Try with public key if it exists - if public_key.exists() { - match Cred::ssh_key(username, Some(&public_key), &private_key, None) - { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key with public key failed: {e}"); - } - } - } - - // Try without public key - match Cred::ssh_key(username, None, &private_key, None) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key without public key failed: {e}"); - } - } - } - } - } else { - eprintln!("No username provided for SSH authentication"); - } - } - - // If username/password authentication is allowed (HTTPS) - if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { - // Try to get credentials from git config or environment - if let (Ok(username), Ok(password)) = - (std::env::var("GIT_USERNAME"), std::env::var("GIT_PASSWORD")) - { - return Cred::userpass_plaintext(&username, &password); - } - - // For GitHub, you might want to use a personal access token - if url.contains("github.com") - && let Ok(token) = std::env::var("GITHUB_TOKEN") - { - return Cred::userpass_plaintext("git", &token); - } - } - - // Default authentication (tries default SSH key) - if allowed_types.contains(CredentialType::DEFAULT) { - match Cred::default() { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("Default authentication failed: {e}"); - } - } - } - - Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - format!( - "Authentication failed after {} attempts for {}. Available methods: {:?}", - current_attempt + 1, - url, - allowed_types - ), - )) - }); - - // Set up certificate check callback for HTTPS - callbacks.certificate_check(|_cert, _host| { - // In production, you should properly validate certificates - // For now, we'll accept all certificates (not recommended for production) - Ok(git2::CertificateCheckStatus::CertificateOk) - }); - - callbacks - } - /// Create fetch options with authentication fn create_fetch_options() -> git2::FetchOptions<'static> { let mut fetch_options = git2::FetchOptions::new(); - fetch_options.remote_callbacks(Self::setup_auth_callbacks()); + fetch_options.remote_callbacks(setup_auth_callbacks()); fetch_options } } diff --git a/src/events/git_pull.rs b/src/events/git_pull.rs index 6180a9e..6e574bf 100644 --- a/src/events/git_pull.rs +++ b/src/events/git_pull.rs @@ -1,9 +1,10 @@ use std::path::Path; use super::AtomicEvent; -use crate::bgit_error::BGitError; +use crate::auth::git_auth::setup_auth_callbacks; +use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; -use git2::{Cred, CredentialType, Repository}; +use git2::Repository; pub struct GitPull { pub pre_check_rules: Vec>, @@ -284,142 +285,10 @@ impl GitPull { Ok(()) } - /// Set up authentication callbacks for git operations - fn setup_auth_callbacks() -> git2::RemoteCallbacks<'static> { - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - let mut callbacks = git2::RemoteCallbacks::new(); - let attempt_count = Arc::new(AtomicUsize::new(0)); - - callbacks.credentials(move |url, username_from_url, allowed_types| { - let current_attempt = attempt_count.fetch_add(1, Ordering::SeqCst); - // Limit authentication attempts to prevent infinite loops - if current_attempt > 3 { - return Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - "Maximum authentication attempts exceeded", - )); - } - - // If SSH key authentication is allowed - if allowed_types.contains(CredentialType::SSH_KEY) { - if let Some(username) = username_from_url { - // Try SSH agent first (most common and secure) - match Cred::ssh_key_from_agent(username) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH agent failed: {e}"); - } - } - - // Try to find SSH keys in standard locations - let home_dir = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .unwrap_or_else(|_| ".".to_string()); - - let ssh_dir = Path::new(&home_dir).join(".ssh"); - - // Common SSH key file names in order of preference - let key_files = [ - ("id_ed25519", "id_ed25519.pub"), - ("id_rsa", "id_rsa.pub"), - ("id_ecdsa", "id_ecdsa.pub"), - ("id_dsa", "id_dsa.pub"), - ]; - - for (private_name, public_name) in &key_files { - let private_key = ssh_dir.join(private_name); - let public_key = ssh_dir.join(public_name); - - if private_key.exists() { - // Try with public key if it exists - if public_key.exists() { - match Cred::ssh_key(username, Some(&public_key), &private_key, None) - { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key with public key failed: {e}"); - } - } - } - - // Try without public key - match Cred::ssh_key(username, None, &private_key, None) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("SSH key without public key failed: {e}"); - } - } - } - } - } else { - eprintln!("No username provided for SSH authentication"); - } - } - - // If username/password authentication is allowed (HTTPS) - if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { - // Try to get credentials from git config or environment - if let (Ok(username), Ok(password)) = - (std::env::var("GIT_USERNAME"), std::env::var("GIT_PASSWORD")) - { - return Cred::userpass_plaintext(&username, &password); - } - - // For GitHub, you might want to use a personal access token - if url.contains("github.com") - && let Ok(token) = std::env::var("GITHUB_TOKEN") - { - return Cred::userpass_plaintext("git", &token); - } - } - - // Default authentication (tries default SSH key) - if allowed_types.contains(CredentialType::DEFAULT) { - match Cred::default() { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - eprintln!("Default authentication failed: {e}"); - } - } - } - - Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - format!( - "Authentication failed after {} attempts for {}. Available methods: {:?}", - current_attempt + 1, - url, - allowed_types - ), - )) - }); - - // Set up certificate check callback for HTTPS - callbacks.certificate_check(|_cert, _host| { - // In production, you should properly validate certificates - // For now, we'll accept all certificates (not recommended for production) - Ok(git2::CertificateCheckStatus::CertificateOk) - }); - - callbacks - } - /// Create fetch options with authentication fn create_fetch_options() -> git2::FetchOptions<'static> { let mut fetch_options = git2::FetchOptions::new(); - fetch_options.remote_callbacks(Self::setup_auth_callbacks()); + fetch_options.remote_callbacks(setup_auth_callbacks()); fetch_options } diff --git a/src/events/git_push.rs b/src/events/git_push.rs index 62f802b..0533bb7 100644 --- a/src/events/git_push.rs +++ b/src/events/git_push.rs @@ -1,10 +1,10 @@ -use std::path::Path; - use super::AtomicEvent; -use crate::bgit_error::BGitError; +use crate::auth::git_auth::setup_auth_callbacks; +use crate::bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP}; use crate::rules::Rule; -use git2::{Cred, CredentialType, Repository}; +use git2::Repository; use log::debug; +use std::path::Path; pub struct GitPush { pub pre_check_rules: Vec>, @@ -220,155 +220,10 @@ impl GitPush { Ok(()) } - /// Set up authentication callbacks for git operations - fn setup_auth_callbacks() -> git2::RemoteCallbacks<'static> { - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - let mut callbacks = git2::RemoteCallbacks::new(); - let attempt_count = Arc::new(AtomicUsize::new(0)); - - callbacks.credentials(move |url, username_from_url, allowed_types| { - let current_attempt = attempt_count.fetch_add(1, Ordering::SeqCst); - // Limit authentication attempts to prevent infinite loops - if current_attempt > 3 { - return Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - "Maximum authentication attempts exceeded", - )); - } - - // If SSH key authentication is allowed - if allowed_types.contains(CredentialType::SSH_KEY) { - if let Some(username) = username_from_url { - // Try SSH agent first (most common and secure) - match Cred::ssh_key_from_agent(username) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH agent failed: {e}"); - } - } - - // Try to find SSH keys in standard locations - let home_dir = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .unwrap_or_else(|_| ".".to_string()); - - let ssh_dir = Path::new(&home_dir).join(".ssh"); - - // Common SSH key file names in order of preference - let key_files = [ - ("id_ed25519", "id_ed25519.pub"), - ("id_rsa", "id_rsa.pub"), - ("id_ecdsa", "id_ecdsa.pub"), - ("id_dsa", "id_dsa.pub"), - ]; - - for (private_name, public_name) in &key_files { - let private_key = ssh_dir.join(private_name); - let public_key = ssh_dir.join(public_name); - - if private_key.exists() { - // Try with public key if it exists - if public_key.exists() { - match Cred::ssh_key(username, Some(&public_key), &private_key, None) - { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH key with public key failed: {e}"); - } - } - } - - // Try without public key - match Cred::ssh_key(username, None, &private_key, None) { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("SSH key without public key failed: {e}"); - } - } - } - } - } else { - println!("No username provided for SSH authentication"); - } - } - - // If username/password authentication is allowed (HTTPS) - if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) { - // Try to get credentials from git config or environment - if let (Ok(username), Ok(password)) = - (std::env::var("GIT_USERNAME"), std::env::var("GIT_PASSWORD")) - { - return Cred::userpass_plaintext(&username, &password); - } - - // For GitHub, you might want to use a personal access token - if url.contains("github.com") - && let Ok(token) = std::env::var("GITHUB_TOKEN") - { - return Cred::userpass_plaintext("git", &token); - } - } - - // Default authentication (tries default SSH key) - if allowed_types.contains(CredentialType::DEFAULT) { - match Cred::default() { - Ok(cred) => { - return Ok(cred); - } - Err(e) => { - println!("Default authentication failed: {e}"); - } - } - } - - Err(git2::Error::new( - git2::ErrorCode::Auth, - git2::ErrorClass::Net, - format!( - "Authentication failed after {} attempts for {}. Available methods: {:?}", - current_attempt + 1, - url, - allowed_types - ), - )) - }); - - // Add push update reference callback for better error reporting - callbacks.push_update_reference(|refname, status| match status { - Some(msg) => { - println!("Push failed for {refname}: {msg}"); - Err(git2::Error::from_str(msg)) - } - None => { - println!("Push successful for {refname}"); - Ok(()) - } - }); - - // Set up certificate check callback for HTTPS - callbacks.certificate_check(|_cert, _host| { - // In production, you should properly validate certificates - // For now, we'll accept all certificates (not recommended for production) - println!("Certificate check for host: {_host}"); - Ok(git2::CertificateCheckStatus::CertificateOk) - }); - - callbacks - } - /// Create push options with authentication fn create_push_options() -> git2::PushOptions<'static> { let mut push_options = git2::PushOptions::new(); - push_options.remote_callbacks(Self::setup_auth_callbacks()); + push_options.remote_callbacks(setup_auth_callbacks()); push_options } } diff --git a/src/main.rs b/src/main.rs index c6554d3..41b0218 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use crate::cmd::log::log; use crate::cmd::{Cli, Commands}; use crate::config::BGitConfig; +mod auth; mod bgit_error; mod cmd; mod config; diff --git a/src/workflows/default/prompt/pa07_ask_pull_push.rs b/src/workflows/default/prompt/pa07_ask_pull_push.rs index 6998fcc..c1bb6aa 100644 --- a/src/workflows/default/prompt/pa07_ask_pull_push.rs +++ b/src/workflows/default/prompt/pa07_ask_pull_push.rs @@ -1,11 +1,11 @@ use crate::config::{StepFlags, WorkflowRules}; +use crate::step::Task::PromptStepTask; use crate::workflows::default::prompt::pa13_pull_push::PullAndPush; use crate::{ bgit_error::{BGitError, BGitErrorWorkflowType, NO_EVENT, NO_RULE}, - step::{PromptStep, Step, Task::PromptStepTask}, + step::{PromptStep, Step}, }; use dialoguer::{Select, theme::ColorfulTheme}; - pub(crate) struct AskPushPull { name: String, } @@ -46,6 +46,7 @@ impl PromptStep for AskPushPull { })?; match selection { + 0 => Ok(Step::Task(PromptStepTask(Box::new(PullAndPush::new())))), 0 => Ok(Step::Task(PromptStepTask(Box::new(PullAndPush::new())))), 1 => Ok(Step::Stop), _ => Err(Box::new(BGitError::new( diff --git a/src/workflows/default/prompt/pa13_pull_push.rs b/src/workflows/default/prompt/pa13_pull_push.rs index a9353a1..20f3367 100644 --- a/src/workflows/default/prompt/pa13_pull_push.rs +++ b/src/workflows/default/prompt/pa13_pull_push.rs @@ -39,25 +39,19 @@ impl PromptStep for PullAndPush { git_pull.add_pre_check_rule(Box::new(RemoteExists::new(workflow_rules_config))); - // Execute pull with rebase match git_pull.execute() { Ok(_) => { - // Pull successful, now attempt push let mut git_push = GitPush::new(); git_push.add_pre_check_rule(Box::new(RemoteExists::new(workflow_rules_config))); git_push.add_pre_check_rule(Box::new(IsRepoSizeTooBig::new(workflow_rules_config))); - // Configure push options - you can customize these as needed git_push .with_force_with_lease(false) .with_upstream_flag(false); match git_push.execute() { - Ok(_) => { - // Both pull and push successful - Ok(Step::Stop) - } + Ok(_) => Ok(Step::Stop), Err(e) => { // Push failed, return error Err(e)