From 7339f7dfc35c0dea98716947b29ba48721a75364 Mon Sep 17 00:00:00 2001 From: James Musselman Date: Wed, 19 Feb 2025 17:02:58 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Feat:=20Add=20basic=20git=20identit?= =?UTF-8?q?y=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 322 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 ++ src/main.rs | 223 ++++++++++++++++++++++++++++++++++++ 3 files changed, 553 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..412d566 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,322 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "git_config_menu" +version = "0.1.0" +dependencies = [ + "dirs", + "toml", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..430af7f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "git_config_menu" +version = "0.1.0" +edition = "2021" + +[dependencies] +dirs = "6.0.0" +toml = "0.8.20" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c73719a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,223 @@ +use std::fs::{create_dir_all, File}; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() { + let config_file = get_config_file_path(); + // Load existing config or create a new one with an empty identities table. + let mut config = load_config(&config_file).unwrap_or_else(create_empty_config); + + loop { + println!("\n=== Git Identity Manager ===\n"); + + // Display the available identities. + if let Some(identities_table) = config.get("identities").and_then(|v| v.as_table()) { + if identities_table.is_empty() { + println!("No identities found."); + } else { + println!("Available identities:"); + let mut keys: Vec<&String> = identities_table.keys().collect(); + keys.sort(); + for (i, key) in keys.iter().enumerate() { + if let Some(identity) = identities_table.get(*key) { + let name = identity + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("N/A"); + let email = identity + .get("email") + .and_then(|v| v.as_str()) + .unwrap_or("N/A"); + println!(" {}. {} <{}>", i + 1, name, email); + } + } + } + } else { + println!("Identities configuration missing."); + } + + println!("\nOptions:"); + println!(" [number] - Select and set that identity"); + println!(" a - Add a new identity"); + println!(" q - Quit"); + + print!("\nEnter your choice: "); + io::stdout().flush().unwrap(); + let mut choice = String::new(); + io::stdin().read_line(&mut choice).unwrap(); + let choice = choice.trim(); + + if choice.eq_ignore_ascii_case("q") { + println!("Exiting without changes."); + break; + } else if choice.eq_ignore_ascii_case("a") { + // Add a new identity. + let (key, new_identity) = add_new_identity(); + { + let identities_table = config + .get_mut("identities") + .and_then(|v| v.as_table_mut()) + .expect("Identities must be a table"); + //FIX: This will overwrite an existing identity with the same key. + identities_table.insert(key.clone(), new_identity.clone()); + } + if let Err(e) = save_config(&config_file, &config) { + println!("Error saving config: {}", e); + break; + } + if let (Some(name), Some(email)) = ( + new_identity.get("name").and_then(|v| v.as_str()), + new_identity.get("email").and_then(|v| v.as_str()), + ) { + if apply_identity(name, email) { + println!("Git identity set to {} <{}>", name, email); + } else { + println!("Failed to set git identity. Make sure you're in a Git repository."); + } + } + break; // Exit after setting an identity. + } else if let Ok(num) = choice.parse::() { + // Selecting an existing identity. + let keys: Vec = { + if let Some(identities_table) = config.get("identities").and_then(|v| v.as_table()) + { + let mut keys: Vec = identities_table.keys().cloned().collect(); + keys.sort(); + keys + } else { + vec![] + } + }; + if num > 0 && num <= keys.len() { + let key = &keys[num - 1]; + if let Some(identity) = config + .get("identities") + .and_then(|v| v.as_table()) + .and_then(|table| table.get(key)) + { + if let (Some(name), Some(email)) = ( + identity.get("name").and_then(|v| v.as_str()), + identity.get("email").and_then(|v| v.as_str()), + ) { + if apply_identity(name, email) { + println!("Git identity set to {} <{}>", name, email); + } else { + println!( + "Failed to set git identity. Make sure you're in a Git repository." + ); + } + } + } + break; // Exit after setting an identity. + } else { + println!("Invalid selection."); + } + } else { + println!("Invalid input."); + } + } +} + +/// Returns the path to the configuration file using the dirs crate. +/// This places the file under: +/// - Linux/Unix: $XDG_CONFIG_HOME/git_identity_manager/git_identities.toml (or $HOME/.config/...) +/// - macOS: $HOME/Library/Application Support/git_identity_manager/git_identities.toml +/// - Windows: %APPDATA%\git_identity_manager\git_identities.toml +fn get_config_file_path() -> PathBuf { + let base_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); + let config_path = base_dir.join("git_identity_manager"); + if let Err(e) = create_dir_all(&config_path) { + eprintln!("Failed to create config directory: {}", e); + } + config_path.join("git_identities.toml") +} + +fn load_config(path: &Path) -> Option { + if !path.exists() { + return None; + } + let mut file = File::open(path).ok()?; + let mut contents = String::new(); + file.read_to_string(&mut contents).ok()?; + toml::from_str(&contents).ok() +} + +fn save_config(path: &Path, config: &toml::Value) -> io::Result<()> { + let toml_string = format_config(config); + let mut file = File::create(path)?; + file.write_all(toml_string.as_bytes()) +} + +/// Formats the configuration in a more conventional TOML style. +/// For example: +/// +/// [identities.The_Linux_Developer] +/// name = "The Linux Developer" +/// email = "email@thelinux.dev" +fn format_config(config: &toml::Value) -> String { + let mut output = String::new(); + if let Some(identities) = config.get("identities").and_then(|v| v.as_table()) { + for (key, value) in identities { + output.push_str(&format!("[identities.{}]\n", key)); + if let Some(inner) = value.as_table() { + for (k, v) in inner { + if let Some(s) = v.as_str() { + output.push_str(&format!("{} = \"{}\"\n", k, s)); + } else { + output.push_str(&format!("{} = {}\n", k, v)); + } + } + } + output.push('\n'); + } + } + output +} + +fn create_empty_config() -> toml::Value { + let mut table = toml::value::Table::new(); + table.insert( + "identities".to_string(), + toml::Value::Table(toml::value::Table::new()), + ); + toml::Value::Table(table) +} + +/// Interactively prompts the user to add a new identity. +/// The entered Git name is used both as the displayed name and, after replacing spaces with underscores, +/// as the key in the configuration. +fn add_new_identity() -> (String, toml::Value) { + println!("\nAdding new identity:"); + print!("Enter the Git name for this identity: "); + io::stdout().flush().unwrap(); + let mut display_name = String::new(); + io::stdin().read_line(&mut display_name).unwrap(); + let display_name = display_name.trim().to_string(); + + print!("Enter your Git email: "); + io::stdout().flush().unwrap(); + let mut email = String::new(); + io::stdin().read_line(&mut email).unwrap(); + let email = email.trim().to_string(); + + //TODO: Sanitize the key by replacing spaces with underscores + let key = display_name.replace(" ", "_"); + let mut id_table = toml::value::Table::new(); + // Use the full display_name for the "name" field + id_table.insert("name".to_string(), toml::Value::String(display_name)); + id_table.insert("email".to_string(), toml::Value::String(email)); + + (key, toml::Value::Table(id_table)) +} + +fn apply_identity(name: &str, email: &str) -> bool { + let status_name = Command::new("git") + .args(["config", "user.name", name]) + .status(); + let status_email = Command::new("git") + .args(["config", "user.email", email]) + .status(); + status_name.map(|s| s.success()).unwrap_or(false) + && status_email.map(|s| s.success()).unwrap_or(false) +}