gitcore 1.5.0

A secure, zero-friction Git identity manager for developers who juggle multiple accounts.
Documentation
use crate::command_runner::{CommandRunner, SystemCommandRunner};
use std::io;

pub struct GpgKey {
    pub id: String,
    pub name: String,
    pub email: String,
}

pub fn list_gpg_keys() -> io::Result<Vec<GpgKey>> {
    list_gpg_keys_with(&SystemCommandRunner)
}

pub(crate) fn list_gpg_keys_with(runner: &dyn CommandRunner) -> io::Result<Vec<GpgKey>> {
    let output = match runner.run("gpg", &["--list-secret-keys", "--with-colons"]) {
        Ok(out) => out,
        Err(_) => return Ok(Vec::new()),
    };

    if !output.status.success() {
        return Ok(Vec::new());
    }

    let content = String::from_utf8_lossy(&output.stdout);
    let mut keys = Vec::new();
    let mut current_id = String::new();

    for line in content.lines() {
        let parts: Vec<&str> = line.split(':').collect();
        if parts.is_empty() {
            continue;
        }

        if parts[0] == "sec" {
            if parts.len() > 4 {
                current_id = parts[4].to_string();
            }
        } else if parts[0] == "uid" && !current_id.is_empty() && parts.len() > 9 {
            let identity = parts[9];
            if let (Some(email_start), Some(email_end)) = (identity.find('<'), identity.find('>')) {
                let name = identity[..email_start].trim().to_string();
                let email = identity[email_start + 1..email_end].trim().to_string();

                keys.push(GpgKey {
                    id: current_id.clone(),
                    name,
                    email,
                });
                current_id = String::new();
            }
        }
    }

    Ok(keys)
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MockCommandRunner {
        stdout: String,
        status_success: bool,
    }

    impl CommandRunner for MockCommandRunner {
        fn run(&self, _command: &str, _args: &[&str]) -> io::Result<std::process::Output> {
            #[cfg(unix)]
            use std::os::unix::process::ExitStatusExt;
            #[cfg(windows)]
            use std::os::windows::process::ExitStatusExt;
            use std::process::ExitStatus;

            Ok(std::process::Output {
                status: ExitStatus::from_raw(if self.status_success { 0 } else { 1 }),
                stdout: self.stdout.as_bytes().to_vec(),
                stderr: Vec::new(),
            })
        }
    }

    #[test]
    fn test_list_gpg_keys_parsing() {
        let sample_output = "\
sec:u:3072:1:C57492EDC2E97855:1584617173:::u:::scESC::::::23::
uid:u::::1584617173::41CD10AD71E738597F876617C57492EDC2E97855::John Doe <john@doe.com>::::::::::
sec:u:4096:1:AABBCCDDEEFF0011:1584617174:::u:::scESC::::::23::
uid:u::::1584617174::41CD10AD71E738597F876617C57492EDC2E97855::Alice Smith <alice@example.com>::::::::::
";
        let runner = MockCommandRunner {
            stdout: sample_output.to_string(),
            status_success: true,
        };

        let keys = list_gpg_keys_with(&runner).unwrap();
        assert_eq!(keys.len(), 2);

        assert_eq!(keys[0].id, "C57492EDC2E97855");
        assert_eq!(keys[0].name, "John Doe");
        assert_eq!(keys[0].email, "john@doe.com");

        assert_eq!(keys[1].id, "AABBCCDDEEFF0011");
        assert_eq!(keys[1].name, "Alice Smith");
        assert_eq!(keys[1].email, "alice@example.com");
    }
}