From 30432b7b187dfa720dd303c88a7755250587c8cf Mon Sep 17 00:00:00 2001 From: kalmenn Date: Mon, 2 Feb 2026 09:01:35 +0100 Subject: [PATCH] add a struct for storing identities, construct it when importing and save --- TODO.md | 5 -- src/data.rs | 140 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 9 +-- src/logging.rs | 2 +- src/subcommands/import.rs | 33 +++++++-- 5 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 src/data.rs diff --git a/TODO.md b/TODO.md index 63f10ba..daa1df1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,5 @@ # Subcommands -## import -Import an identity from a gpg key. - -We still need to save it somewhere - ## use Set the identity of the current git repo diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..a029434 --- /dev/null +++ b/src/data.rs @@ -0,0 +1,140 @@ +use std::{ + fmt::Display, + fs::File, + io::{Read as _, Write as _}, + path::PathBuf, + sync::LazyLock, +}; + +use serde::{Deserialize, Serialize}; + +use crate::cli::THEME; + +pub static DATA_FOLDER: LazyLock = LazyLock::new(|| { + let mut dir = dirs::data_dir().expect(""); + dir.push("git-identity"); + dir +}); + +pub static IDENTITIES_FOLDER: LazyLock = LazyLock::new(|| { + let mut dir = DATA_FOLDER.clone(); + dir.push("identities"); + dir +}); + +#[derive(Debug)] +pub struct Identity { + pub alias: String, + pub data: IdentityData, +} + +#[derive(thiserror::Error, Debug)] +pub enum IdentityOpenError { + #[error("IO error: {0}")] + IO(#[from] std::io::Error), + + #[error("error while deserializing: {0}")] + Deserialize(#[from] toml::de::Error), +} + +impl Identity { + pub fn open(alias: &str) -> Result + where + A: Into, + { + let alias = String::from(alias); + + let path = { + let mut path = DATA_FOLDER.clone(); + path.push("identities"); + path.push(&alias); + path + }; + + let mut file = File::options() + .read(true) + .write(false) + .create(false) + .open(path)?; + + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + let data = IdentityData::deserialize(toml::Deserializer::parse(&contents)?)?; + + Ok(Self { alias, data }) + } + + pub fn get_path(&self) -> PathBuf { + let mut path = IDENTITIES_FOLDER.clone(); + path.push(&self.alias); + path + } + + pub fn save(&self) -> std::io::Result<()> { + let contents = toml::to_string(&self.data) + .expect("there should only be serializable values in the internal data structure"); + + let path = self.get_path(); + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + File::options() + .write(true) + .truncate(true) + .append(false) + .create(true) + .open(&path)? + .write_all(contents.as_bytes())?; + + Ok(()) + } + + pub fn save_interactively(&self) -> anyhow::Result<()> { + let path = self.get_path(); + + if std::fs::exists(&path)? + && !dialoguer::Confirm::with_theme(&*THEME) + .with_prompt(format!("{} already exists. Ovewrite?", path.display())) + .interact()? + { + Ok(()) + } else { + Ok(self.save()?) + } + } +} + +impl Display for Identity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\":", self.alias)?; + + if let Some(ref name) = self.data.name { + write!(f, " {name}")?; + } + + if let Some(ref email) = self.data.email { + write!(f, " <{email}>")?; + } + + if let Some(ref sigkey) = self.data.sigkey { + write!(f, " (sigkey: {sigkey})")?; + } + + if let Some(ref authkey) = self.data.authkey { + write!(f, " (authkey: {})", authkey.display())?; + } + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct IdentityData { + pub name: Option, + pub email: Option, + pub sigkey: Option, + pub authkey: Option, +} diff --git a/src/lib.rs b/src/lib.rs index c7c54a3..ed4d21a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,7 @@ //! This crate is not a library but we need some things to be exported so that they can be read by //! cargo xtasks -use std::{path::PathBuf, sync::LazyLock}; - pub mod cli; +pub mod data; pub mod logging; pub mod subcommands; - -pub static DATA_FOLDER: LazyLock = LazyLock::new(|| { - let mut dir = dirs::data_dir().expect(""); - dir.push("git-identity"); - dir -}); diff --git a/src/logging.rs b/src/logging.rs index 27e4bed..0b00787 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -4,7 +4,7 @@ use tracing_subscriber::{Layer as _, layer::SubscriberExt as _, util::Subscriber pub const DEFAULT_LOG_LEVEL: Level = if cfg!(debug_assertions) { Level::DEBUG // debug builds } else { - Level::WARN // release builds + Level::INFO // release builds }; fn level_to_index(level: Level) -> u8 { diff --git a/src/subcommands/import.rs b/src/subcommands/import.rs index 68470ae..fbbd7c8 100644 --- a/src/subcommands/import.rs +++ b/src/subcommands/import.rs @@ -25,17 +25,27 @@ pub fn main(_global_cli: crate::cli::GlobalArgs, cli: crate::cli::ImportCli) -> .to_str() .expect("this should always be valid UTF-8"); - let identity_name = dialoguer::Input::::with_theme(&*THEME) + let alias = dialoguer::Input::::with_theme(&*THEME) .with_prompt("What should this identity be named?") .interact_text()?; - let authkey: Option = select_authkey(&mut ctx, &key, &identity_name)?; + let authkey: Option = select_authkey(&mut ctx, &key, &alias)?; let (name, email) = select_name_and_email(&key)?; - info!( - "Imported identity: [{identity_name}] {name:?} <{email:?}> (signing key: {fingerprint}, authentication key: {authkey:?})" - ); + let identity = crate::data::Identity { + alias, + data: crate::data::IdentityData { + name, + email, + sigkey: Some(fingerprint.to_owned()), + authkey, + }, + }; + + identity.save_interactively()?; + + info!("Imported identity {identity}"); Ok(()) } @@ -207,6 +217,17 @@ fn select_authkey( ) -> anyhow::Result> { let mut authkey_prompt = dialoguer::Input::::with_theme(&*THEME) .with_prompt("Select an SSH key (leave empty for none)") + .validate_with(|input: &String| -> Result<(), Cow> { + if input.is_empty() { + return Ok(()); + } + + match std::fs::exists(input) { + Ok(true) => Ok(()), + Ok(false) => Err(Cow::Borrowed("file not found")), + Err(err) => Err(Cow::Owned(err.to_string())), + } + }) .allow_empty(true); if let Some(authkey) = try_export_as_ssh_key(ctx, key, identity_name)? { @@ -248,7 +269,7 @@ fn try_export_as_ssh_key( } let default_destination = { - let mut path = crate::DATA_FOLDER.clone(); + let mut path = crate::data::DATA_FOLDER.clone(); path.push("ssh"); path.push(format!("{identity_name}.pub")); path