a73x

src/identity.rs

Ref:   Size: 5.9 KiB

use std::fs;
use std::path::PathBuf;

use base64::Engine;
use git2::Repository;

use crate::error::Error;
use crate::event::Author;
use crate::signing;

pub fn get_author(repo: &Repository) -> Result<Author, git2::Error> {
    let config = repo.config()?;
    let name = config.get_string("user.name")?;
    let email = config.get_string("user.email")?;
    Ok(Author { name, email })
}

pub fn author_signature(author: &Author) -> Result<git2::Signature<'static>, git2::Error> {
    git2::Signature::now(&author.name, &author.email)
}

/// Path to the identity-aliases file inside the repo's collab directory.
fn aliases_path(repo: &Repository) -> PathBuf {
    repo.path().join("collab").join("identity-aliases")
}

/// Load the list of email aliases from `.git/collab/identity-aliases`.
/// Returns an empty vec if the file doesn't exist.
pub fn load_aliases(repo: &Repository) -> Result<Vec<String>, Error> {
    let path = aliases_path(repo);
    if !path.exists() {
        return Ok(Vec::new());
    }
    let contents = fs::read_to_string(&path)?;
    let aliases: Vec<String> = contents
        .lines()
        .map(|l| l.trim().to_string())
        .filter(|l| !l.is_empty())
        .collect();
    Ok(aliases)
}

/// Save the list of email aliases to `.git/collab/identity-aliases`.
fn save_aliases(repo: &Repository, aliases: &[String]) -> Result<(), Error> {
    let path = aliases_path(repo);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let contents = aliases.join("\n");
    let contents = if contents.is_empty() {
        contents
    } else {
        format!("{}\n", contents)
    };
    fs::write(&path, contents)?;
    Ok(())
}

/// Add an email alias for the current identity. Returns true if added, false if already present.
pub fn add_alias(repo: &Repository, email: &str) -> Result<bool, Error> {
    let author = get_author(repo)?;
    if email == author.email {
        return Err(Error::Cmd(format!(
            "'{}' is already your primary email; cannot add as alias",
            email
        )));
    }
    let mut aliases = load_aliases(repo)?;
    if aliases.iter().any(|a| a == email) {
        return Ok(false);
    }
    aliases.push(email.to_string());
    save_aliases(repo, &aliases)?;
    Ok(true)
}

/// Remove an email alias. Returns an error if the alias is not found.
pub fn remove_alias(repo: &Repository, email: &str) -> Result<(), Error> {
    let mut aliases = load_aliases(repo)?;
    let before = aliases.len();
    aliases.retain(|a| a != email);
    if aliases.len() == before {
        return Err(Error::Cmd(format!(
            "alias '{}' not found",
            email
        )));
    }
    save_aliases(repo, &aliases)?;
    Ok(())
}

/// Display full identity information: name, email, signing key, aliases.
pub fn whoami(repo: &Repository) -> Result<String, Error> {
    let author = get_author(repo)?;
    let mut lines = Vec::new();
    lines.push(format!("Name: {}", author.name));
    lines.push(format!("Email: {}", author.email));

    // Signing key
    match signing::signing_key_dir() {
        Ok(config_dir) => match signing::load_verifying_key(&config_dir) {
            Ok(vk) => {
                let pubkey_b64 =
                    base64::engine::general_purpose::STANDARD.encode(vk.to_bytes());
                lines.push(format!("Signing key: {}", pubkey_b64));
            }
            Err(_) => {
                lines.push("Signing key: (not configured)".to_string());
            }
        },
        Err(_) => {
            lines.push("Signing key: (not configured)".to_string());
        }
    }

    // Aliases
    let aliases = load_aliases(repo)?;
    if aliases.is_empty() {
        lines.push("Aliases: (none)".to_string());
    } else {
        lines.push("Aliases:".to_string());
        for alias in &aliases {
            lines.push(format!("  {}", alias));
        }
    }

    Ok(lines.join("\n"))
}

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

    fn test_repo(name: &str, email: &str) -> (TempDir, Repository) {
        let dir = TempDir::new().unwrap();
        let repo = Repository::init(dir.path()).unwrap();
        {
            let mut config = repo.config().unwrap();
            config.set_str("user.name", name).unwrap();
            config.set_str("user.email", email).unwrap();
        }
        (dir, repo)
    }

    #[test]
    fn test_load_aliases_empty_when_no_file() {
        let (_dir, repo) = test_repo("Test", "test@example.com");
        let aliases = load_aliases(&repo).unwrap();
        assert!(aliases.is_empty());
    }

    #[test]
    fn test_add_and_load_alias() {
        let (_dir, repo) = test_repo("Test", "test@example.com");
        let added = add_alias(&repo, "other@example.com").unwrap();
        assert!(added);
        let aliases = load_aliases(&repo).unwrap();
        assert_eq!(aliases, vec!["other@example.com"]);
    }

    #[test]
    fn test_add_duplicate_alias_returns_false() {
        let (_dir, repo) = test_repo("Test", "test@example.com");
        add_alias(&repo, "other@example.com").unwrap();
        let added = add_alias(&repo, "other@example.com").unwrap();
        assert!(!added);
    }

    #[test]
    fn test_add_primary_email_errors() {
        let (_dir, repo) = test_repo("Test", "test@example.com");
        let result = add_alias(&repo, "test@example.com");
        assert!(result.is_err());
    }

    #[test]
    fn test_remove_alias() {
        let (_dir, repo) = test_repo("Test", "test@example.com");
        add_alias(&repo, "other@example.com").unwrap();
        remove_alias(&repo, "other@example.com").unwrap();
        let aliases = load_aliases(&repo).unwrap();
        assert!(aliases.is_empty());
    }

    #[test]
    fn test_remove_nonexistent_alias_errors() {
        let (_dir, repo) = test_repo("Test", "test@example.com");
        let result = remove_alias(&repo, "nobody@example.com");
        assert!(result.is_err());
    }
}