use anyhow::Result;
use std::path::{Path, PathBuf};
use std::future::Future;
use std::pin::Pin;
use super::operations::{get_git_config, run_git, set_git_config};
use super::status::Status;
pub type PromptFn = Box<dyn Fn(&str, &UserConfig, &UserConfig) -> Pin<Box<dyn Future<Output = Result<bool>> + Send>> + Send + Sync>;
#[derive(Clone, Debug)]
pub struct UserConfig {
pub name: Option<String>,
pub email: Option<String>,
}
impl UserConfig {
pub fn new(name: Option<String>, email: Option<String>) -> Self {
Self { name, email }
}
pub fn is_empty(&self) -> bool {
self.name.is_none() && self.email.is_none()
}
}
#[derive(Clone)]
pub enum ConfigSource {
Explicit(UserConfig),
Global,
Current(PathBuf),
Interactive,
}
#[derive(Clone)]
pub enum ConfigCommand {
Interactive(ConfigSource),
Force(ConfigSource),
DryRun(ConfigSource),
}
#[derive(Clone)]
pub struct ConfigArgs {
pub command: ConfigCommand,
}
pub async fn get_current_user_config(path: &Path) -> (Option<String>, Option<String>) {
let name = get_git_config(path, "user.name").await.unwrap_or(None);
let email = get_git_config(path, "user.email").await.unwrap_or(None);
(name, email)
}
pub async fn get_global_user_config() -> (Option<String>, Option<String>) {
let temp_dir = std::env::temp_dir();
let name = match run_git(&temp_dir, &["config", "--global", "--get", "user.name"]).await {
Ok((true, value, _)) if !value.is_empty() => Some(value),
_ => None,
};
let email = match run_git(&temp_dir, &["config", "--global", "--get", "user.email"]).await {
Ok((true, value, _)) if !value.is_empty() => Some(value),
_ => None,
};
(name, email)
}
pub fn validate_user_config(config: &UserConfig) -> Result<()> {
if let Some(name) = &config.name {
if name.trim().is_empty() {
return Err(anyhow::anyhow!("User name cannot be empty"));
}
}
if let Some(email) = &config.email {
let email = email.trim();
if email.is_empty() {
return Err(anyhow::anyhow!("User email cannot be empty"));
}
if !email.contains('@') || !email.split('@').nth(1).unwrap_or("").contains('.') {
return Err(anyhow::anyhow!("Invalid email format: {}", email));
}
}
Ok(())
}
pub async fn check_repo_config(
path: &Path,
repo_name: &str,
target_config: &UserConfig,
command: &ConfigCommand,
prompt_fn: Option<&PromptFn>,
) -> (Status, String) {
let (current_name, current_email) = get_current_user_config(path).await;
let current_config = UserConfig::new(current_name, current_email);
let name_needs_update = match (¤t_config.name, &target_config.name) {
(Some(current), Some(target)) => current != target,
(None, Some(_)) => true,
_ => false,
};
let email_needs_update = match (¤t_config.email, &target_config.email) {
(Some(current), Some(target)) => current != target,
(None, Some(_)) => true,
_ => false,
};
if !name_needs_update && !email_needs_update {
return (Status::ConfigSynced, "config synced".to_string());
}
if matches!(command, ConfigCommand::DryRun(_)) {
let mut changes = Vec::new();
if name_needs_update {
if let Some(target_name) = &target_config.name {
changes.push(format!("name → {}", target_name));
}
}
if email_needs_update {
if let Some(target_email) = &target_config.email {
changes.push(format!("email → {}", target_email));
}
}
return (
Status::ConfigSkipped,
format!("would update: {}", changes.join(", ")),
);
}
let should_update = match command {
ConfigCommand::Force(_) => true,
ConfigCommand::Interactive(_) => {
if let Some(prompt) = prompt_fn {
prompt(repo_name, ¤t_config, target_config)
.await
.unwrap_or(false)
} else {
false
}
}
ConfigCommand::DryRun(_) => false, };
if !should_update {
return (Status::ConfigSkipped, "config unchanged".to_string());
}
let mut updates = Vec::new();
let mut errors = Vec::new();
if name_needs_update {
if let Some(target_name) = &target_config.name {
match set_git_config(path, "user.name", target_name).await {
Ok(true) => updates.push("name"),
Ok(false) | Err(_) => errors.push("name"),
}
}
}
if email_needs_update {
if let Some(target_email) = &target_config.email {
match set_git_config(path, "user.email", target_email).await {
Ok(true) => updates.push("email"),
Ok(false) | Err(_) => errors.push("email"),
}
}
}
if !errors.is_empty() {
(
Status::ConfigError,
format!("failed to update: {}", errors.join(", ")),
)
} else {
(
Status::ConfigUpdated,
format!("updated: {}", updates.join(", ")),
)
}
}
#[allow(dead_code)]
pub fn is_valid_email(email: &str) -> bool {
if email.is_empty() {
return false;
}
let parts: Vec<&str> = email.split('@').collect();
parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() && parts[1].contains('.')
}
#[allow(dead_code)]
pub fn is_valid_name(name: &str) -> bool {
!name.trim().is_empty()
}