use std::collections::HashMap;
use crate::{RpoError, backend::Signature};
#[derive(Clone, Debug, Default)]
pub struct IdentityMap {
by_email: HashMap<String, (String, String)>,
by_name: HashMap<String, (String, String)>,
}
impl IdentityMap {
pub fn new() -> Self {
Self::default()
}
pub fn insert_by_email(
&mut self,
raw_email: impl Into<String>,
canonical_name: impl Into<String>,
canonical_email: impl Into<String>,
) {
let key = raw_email.into().to_lowercase();
self.by_email
.insert(key, (canonical_name.into(), canonical_email.into()));
}
pub fn insert_by_name(
&mut self,
raw_name: impl Into<String>,
canonical_name: impl Into<String>,
canonical_email: impl Into<String>,
) {
self.by_name.insert(
raw_name.into(),
(canonical_name.into(), canonical_email.into()),
);
}
fn resolve(&self, raw_name: &str, raw_email: &str) -> Option<(String, String)> {
if let Some(hit) = self.by_email.get(&raw_email.to_lowercase()) {
return Some(hit.clone());
}
if let Some(hit) = self.by_name.get(raw_name) {
return Some(hit.clone());
}
None
}
}
pub struct Canonicalizer {
#[cfg(feature = "backend-gix")]
mailmap: Option<gix_mailmap::Snapshot>,
overrides: IdentityMap,
}
impl Canonicalizer {
pub fn new(mailmap_bytes: Option<&[u8]>, overrides: IdentityMap) -> Result<Self, RpoError> {
#[cfg(feature = "backend-gix")]
let mailmap = mailmap_bytes.map(gix_mailmap::Snapshot::from_bytes);
#[cfg(not(feature = "backend-gix"))]
let _ = mailmap_bytes;
Ok(Self {
#[cfg(feature = "backend-gix")]
mailmap,
overrides,
})
}
pub fn canonicalize(&self, sig: &Signature) -> (String, String) {
let mut name = sig.name.clone();
let mut email = sig.email.clone();
#[cfg(feature = "backend-gix")]
if let Some(mm) = &self.mailmap {
let as_ref = gix::actor::SignatureRef {
name: name.as_bytes().into(),
email: email.as_bytes().into(),
time: "",
};
if let Some(resolved) = mm.try_resolve(as_ref) {
name = resolved.name.to_string();
email = resolved.email.to_string();
}
}
if let Some((on, oe)) = self.overrides.resolve(&name, &email) {
name = on;
email = oe;
}
(name, email)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sig(name: &str, email: &str) -> Signature {
Signature {
name: name.into(),
email: email.into(),
time_ms: 0,
}
}
#[test]
fn raw_passthrough_when_no_mapping() {
let c = Canonicalizer::new(None, IdentityMap::new()).unwrap();
let (n, e) = c.canonicalize(&sig("Alice", "alice@a.com"));
assert_eq!(n, "Alice");
assert_eq!(e, "alice@a.com");
}
#[test]
fn override_by_email_remaps() {
let mut m = IdentityMap::new();
m.insert_by_email("old@a.com", "Alice Canonical", "alice@canonical.com");
let c = Canonicalizer::new(None, m).unwrap();
let (n, e) = c.canonicalize(&sig("whoever", "OLD@A.COM"));
assert_eq!(n, "Alice Canonical");
assert_eq!(e, "alice@canonical.com");
}
#[cfg(feature = "backend-gix")]
#[test]
fn mailmap_applied() {
let bytes = b"Alice Real <alice@real.com> <alice@old.com>\n";
let c = Canonicalizer::new(Some(bytes), IdentityMap::new()).unwrap();
let (n, e) = c.canonicalize(&sig("Whatever", "alice@old.com"));
assert_eq!(n, "Alice Real");
assert_eq!(e, "alice@real.com");
}
#[cfg(feature = "backend-gix")]
#[test]
fn override_wins_over_mailmap() {
let bytes = b"Alice Mail <alice@mail.com> <alice@old.com>\n";
let mut m = IdentityMap::new();
m.insert_by_email("alice@mail.com", "Alice Override", "alice@override.com");
let c = Canonicalizer::new(Some(bytes), m).unwrap();
let (n, e) = c.canonicalize(&sig("x", "alice@old.com"));
assert_eq!(n, "Alice Override");
assert_eq!(e, "alice@override.com");
}
}