use std::path::Path;
use std::process::Command;
pub fn canonical_author_email_in(cwd: &Path, name: &str, email: &str) -> Option<String> {
if email.is_empty() {
return None;
}
let mut child = Command::new("git")
.current_dir(cwd)
.args(["check-mailmap", "--stdin"])
.env("GIT_TERMINAL_PROMPT", "0")
.env("LC_ALL", "C")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.ok()?;
{
use std::io::Write as _;
let mut stdin = child.stdin.take()?;
writeln!(stdin, "{name} <{email}>").ok()?;
}
let output = child.wait_with_output().ok()?;
if !output.status.success() {
return None;
}
let line = String::from_utf8_lossy(&output.stdout);
parse_contact_email(line.trim())
}
fn parse_contact_email(line: &str) -> Option<String> {
let open = line.rfind('<')?;
let close = line[open..].find('>')? + open;
let email = line[open + 1..close].trim();
if email.is_empty() {
None
} else {
Some(email.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn git(dir: &Path, args: &[&str]) {
let out = Command::new("git")
.current_dir(dir)
.args(args)
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.unwrap();
assert!(
out.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
}
fn init_repo_with_mailmap(dir: &Path, mailmap: &str) {
git(dir, &["init", "-q"]);
git(dir, &["config", "user.email", "test@test.com"]);
git(dir, &["config", "user.name", "test"]);
std::fs::write(dir.join(".mailmap"), mailmap).unwrap();
}
#[test]
fn maps_aliased_email_to_canonical() {
let tmp = tempfile::tempdir().unwrap();
init_repo_with_mailmap(tmp.path(), "TJ Smith <tj@jarvispro.io> <jane@work.com>\n");
assert_eq!(
canonical_author_email_in(tmp.path(), "TJ Smith", "jane@work.com"),
Some("tj@jarvispro.io".to_string()),
"aliased email canonicalizes to the primary identity"
);
}
#[test]
fn unmapped_email_passes_through_unchanged() {
let tmp = tempfile::tempdir().unwrap();
init_repo_with_mailmap(tmp.path(), "TJ Smith <tj@jarvispro.io> <jane@work.com>\n");
assert_eq!(
canonical_author_email_in(tmp.path(), "Someone", "nobody@example.com"),
Some("nobody@example.com".to_string()),
"an unmapped identity echoes back unchanged (no-op)"
);
}
#[test]
fn no_mailmap_is_noop() {
let tmp = tempfile::tempdir().unwrap();
git(tmp.path(), &["init", "-q"]);
git(tmp.path(), &["config", "user.email", "test@test.com"]);
git(tmp.path(), &["config", "user.name", "test"]);
assert_eq!(
canonical_author_email_in(tmp.path(), "TJ Smith", "jane@work.com"),
Some("jane@work.com".to_string()),
"without a .mailmap the input email is returned verbatim"
);
}
#[test]
fn non_repo_dir_returns_none() {
let tmp = tempfile::tempdir().unwrap();
assert_eq!(
canonical_author_email_in(tmp.path(), "TJ Smith", "jane@work.com"),
None,
"a non-repo directory disables canonicalization (best-effort)"
);
}
#[test]
fn empty_email_returns_none() {
let tmp = tempfile::tempdir().unwrap();
init_repo_with_mailmap(tmp.path(), "TJ Smith <tj@x.io> <j@w.com>\n");
assert_eq!(canonical_author_email_in(tmp.path(), "TJ Smith", ""), None);
}
#[test]
fn parse_contact_email_extracts_address() {
assert_eq!(
parse_contact_email("TJ Smith <tj@x.io>"),
Some("tj@x.io".to_string())
);
assert_eq!(parse_contact_email("No Email Here"), None);
assert_eq!(parse_contact_email("Empty <>"), None);
}
}