use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::fs as platform_fs;
#[cfg(unix)]
use std::os::unix::fs as platform_fs;
use crate::config::{get_commit_config, mappings, CommitConfig};
use crate::utils::git_repo::find_git_repo_root;
#[derive(Args)]
pub struct ConfigCommand {
#[command(subcommand)]
command: ConfigSubcommand,
}
#[derive(Subcommand)]
enum ConfigSubcommand {
Init {
#[arg(long, short)]
global: bool,
},
Show,
List,
Use {
name: String,
},
AddMapping {
name: String,
path: Option<String>,
},
ListMappings,
}
impl ConfigCommand {
pub async fn execute(&self) -> Result<()> {
match &self.command {
ConfigSubcommand::Init { global } => Self::handle_init(*global),
ConfigSubcommand::Show => Self::handle_show(),
ConfigSubcommand::List => Self::handle_list(),
ConfigSubcommand::Use { name } => Self::handle_use(name),
ConfigSubcommand::AddMapping { name, path } => Self::handle_add_mapping(name, path),
ConfigSubcommand::ListMappings => Self::handle_list_mappings(),
}?;
Ok(())
}
fn handle_init(global: bool) -> Result<()> {
let target_path = if global {
let config_dir = Self::get_config_dir()?;
std::fs::create_dir_all(&config_dir)?;
config_dir.join("default.fuckmit.yml")
} else {
PathBuf::from(".fuckmit.yml")
};
if target_path.exists() {
println!(
"Commit configuration already exists at {}",
target_path.display()
);
} else {
let commit_config = CommitConfig::default();
let yaml = serde_yaml::to_string(&commit_config)?;
std::fs::write(&target_path, yaml)?;
println!("Created new config file at {}", target_path.display());
}
if global {
let config_dir = Self::get_config_dir()?;
let symlink_path = config_dir.join(".fuckmit.yml");
if symlink_path.exists() {
let _ = std::fs::remove_file(&symlink_path);
}
if let Err(e) = Self::create_symlink(&target_path, &symlink_path) {
eprintln!("Warning: Could not create symlink: {}", e);
} else {
println!("Set as active configuration");
}
}
Ok(())
}
fn handle_show() -> Result<()> {
let commit_config = get_commit_config()?;
println!("{}", serde_yaml::to_string(&commit_config)?);
Ok(())
}
fn handle_list() -> Result<()> {
let config_dir = Self::get_config_dir()?;
let entries = std::fs::read_dir(&config_dir).context(format!(
"Failed to read config directory: {}",
config_dir.display()
))?;
let mut configs = Vec::new();
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_file() && Self::is_config_file(&path) {
configs.push(path);
}
}
let symlink_path = config_dir.join(".fuckmit.yml");
let active_target = if symlink_path.exists() && symlink_path.is_symlink() {
std::fs::read_link(&symlink_path).ok().map(|p| {
p.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
})
} else {
None
};
println!("Available configurations:\n");
let has_configs = !configs.is_empty();
for config_path in &configs {
let file_name = config_path
.file_name()
.unwrap_or_default()
.to_string_lossy();
let is_active = match &active_target {
Some(target) if target == &file_name => " (active)",
_ => "",
};
let display_name = file_name.to_string();
let display_name = display_name
.strip_suffix(".fuckmit.yml")
.or_else(|| display_name.strip_suffix(".fuckmit.yaml"))
.unwrap_or(&display_name);
println!(" {}{}", display_name, is_active);
}
if !has_configs {
println!("No configurations found. Create one with 'fuckmit config init --global'.");
}
Ok(())
}
fn handle_use(name: &str) -> Result<()> {
let config_dir = Self::get_config_dir()?;
std::fs::create_dir_all(&config_dir)?;
let source_file = if name.ends_with(".fuckmit.yml") || name.ends_with(".fuckmit.yaml") {
config_dir.join(name)
} else {
config_dir.join(format!("{}.fuckmit.yml", name))
};
if !source_file.exists() {
return Err(anyhow::anyhow!("Configuration '{}' not found", name));
}
let symlink_path = config_dir.join(".fuckmit.yml");
if symlink_path.exists() {
let _ = std::fs::remove_file(&symlink_path);
}
match Self::create_symlink(&source_file, &symlink_path) {
Ok(_) => {}
Err(e) => {
return Err(anyhow::anyhow!(
"Failed to create symlink from {} to {}: {}",
source_file.display(),
symlink_path.display(),
e
));
}
};
println!("Now using '{}' as the active configuration", name);
Ok(())
}
fn handle_add_mapping(name: &str, path: &Option<String>) -> Result<()> {
let repo_path = match path {
Some(p) => PathBuf::from(p),
None => find_git_repo_root(None)?,
};
if !repo_path.exists() || !repo_path.is_dir() {
return Err(anyhow::anyhow!(
"Path '{}' does not exist or is not a directory",
repo_path.display()
));
}
if !repo_path.join(".git").exists() {
return Err(anyhow::anyhow!(
"Path '{}' is not a Git repository",
repo_path.display()
));
}
let repo_path = std::fs::canonicalize(&repo_path).context(format!(
"Failed to canonicalize path: {}",
repo_path.display()
))?;
let repo_path_str = repo_path.to_string_lossy().to_string();
let mut mappings = mappings::load()?;
mappings
.mappings
.insert(repo_path_str.clone(), name.to_string());
mappings::save(&mappings)?;
println!("Added mapping: '{}' -> '{}'", repo_path_str, name);
Ok(())
}
fn handle_list_mappings() -> Result<()> {
let mappings = mappings::load()?;
if mappings.mappings.is_empty() {
println!("No mappings found. Add one with 'fuckmit config add-mapping <name> [path]'.");
return Ok(());
}
println!("Available mappings:\n");
for (path, name) in &mappings.mappings {
println!(" '{}' -> '{}'", path, name);
}
Ok(())
}
fn get_config_dir() -> Result<PathBuf> {
if let Ok(config_dir) = std::env::var("FUCKMIT_CONFIG_DIR") {
let mut path = PathBuf::from(config_dir);
path.push("fuckmit");
return Ok(path);
}
let mut path = dirs::config_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?;
path.push("fuckmit");
Ok(path)
}
fn is_config_file(path: &Path) -> bool {
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
file_name.ends_with(".fuckmit.yml") || file_name.ends_with(".fuckmit.yaml")
}
fn create_symlink(source: &Path, dest: &Path) -> Result<()> {
#[cfg(unix)]
{
platform_fs::symlink(source, dest)
.map_err(|e| anyhow::anyhow!("Failed to create symlink: {}", e))
}
#[cfg(windows)]
{
platform_fs::copy(source, dest)
.map(|_| ())
.map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))
}
}
}