diff --git a/gitoxide-core/src/repository/mailmap.rs b/gitoxide-core/src/repository/mailmap.rs index f9c947d8502..668ccd3c086 100644 --- a/gitoxide-core/src/repository/mailmap.rs +++ b/gitoxide-core/src/repository/mailmap.rs @@ -1,3 +1,5 @@ +use anyhow::bail; +use gix::bstr::{BString, ByteSlice}; use std::io; #[cfg(feature = "serde")] @@ -50,3 +52,59 @@ pub fn entries( Ok(()) } + +pub fn check( + repo: gix::Repository, + format: OutputFormat, + contacts: Vec, + mut out: impl io::Write, + mut err: impl io::Write, +) -> anyhow::Result<()> { + if format != OutputFormat::Human { + bail!("Only human output is supported right now"); + } + if contacts.is_empty() { + bail!("specify at least one contact to run through the mailmap") + } + + let mut mailmap = gix::mailmap::Snapshot::default(); + if let Err(err) = repo.open_mailmap_into(&mut mailmap) { + bail!(err); + } + + let mut buf = Vec::new(); + for contact in contacts { + let actor = match gix::actor::IdentityRef::from_bytes::<()>(&contact) { + Ok(a) => a, + Err(_) => { + let Some(email) = contact + .trim_start() + .strip_prefix(b"<") + .and_then(|rest| rest.trim_end().strip_suffix(b">")) + else { + writeln!(err, "Failed to parse contact '{contact}' - skipping")?; + continue; + }; + gix::actor::IdentityRef { + name: "".into(), + email: email.into(), + } + } + }; + let resolved = mailmap.resolve_cow(gix::actor::SignatureRef { + name: actor.name, + email: actor.email, + time: Default::default(), + }); + let resolved = gix::actor::IdentityRef { + name: resolved.name.as_ref(), + email: resolved.email.as_ref(), + }; + buf.clear(); + resolved.write_to(&mut buf)?; + + out.write_all(&buf)?; + out.write_all(b"\n")?; + } + Ok(()) +} diff --git a/gix-mailmap/src/entry.rs b/gix-mailmap/src/entry.rs index 69a0c53ee57..9e219b63b33 100644 --- a/gix-mailmap/src/entry.rs +++ b/gix-mailmap/src/entry.rs @@ -25,8 +25,8 @@ impl<'a> Entry<'a> { /// Constructors indicating what kind of mapping is created. /// /// Only these combinations of values are valid. -#[allow(missing_docs)] impl<'a> Entry<'a> { + /// An entry that changes the name by an email. pub fn change_name_by_email(proper_name: impl Into<&'a BStr>, commit_email: impl Into<&'a BStr>) -> Self { Entry { new_name: Some(proper_name.into()), @@ -34,6 +34,7 @@ impl<'a> Entry<'a> { ..Default::default() } } + /// An entry that changes the email by an email. pub fn change_email_by_email(proper_email: impl Into<&'a BStr>, commit_email: impl Into<&'a BStr>) -> Self { Entry { new_email: Some(proper_email.into()), @@ -41,6 +42,20 @@ impl<'a> Entry<'a> { ..Default::default() } } + /// An entry that changes the email by a name and email. + pub fn change_email_by_name_and_email( + proper_email: impl Into<&'a BStr>, + commit_name: impl Into<&'a BStr>, + commit_email: impl Into<&'a BStr>, + ) -> Self { + Entry { + new_email: Some(proper_email.into()), + old_email: commit_email.into(), + old_name: Some(commit_name.into()), + ..Default::default() + } + } + /// An entry that changes a name and the email by an email. pub fn change_name_and_email_by_email( proper_name: impl Into<&'a BStr>, proper_email: impl Into<&'a BStr>, @@ -53,7 +68,7 @@ impl<'a> Entry<'a> { ..Default::default() } } - + /// An entry that changes a name and email by a name and email. pub fn change_name_and_email_by_name_and_email( proper_name: impl Into<&'a BStr>, proper_email: impl Into<&'a BStr>, diff --git a/gix-mailmap/src/parse.rs b/gix-mailmap/src/parse.rs index c6752f6f39c..34d3c417619 100644 --- a/gix-mailmap/src/parse.rs +++ b/gix-mailmap/src/parse.rs @@ -77,6 +77,9 @@ fn parse_line(line: &BStr, line_number: usize) -> Result, Error> { (Some(proper_name), Some(proper_email), Some(commit_name), Some(commit_email)) => { Entry::change_name_and_email_by_name_and_email(proper_name, proper_email, commit_name, commit_email) } + (None, Some(proper_email), Some(commit_name), Some(commit_email)) => { + Entry::change_email_by_name_and_email(proper_email, commit_name, commit_email) + } _ => { return Err(Error::Malformed { line_number, diff --git a/gix-mailmap/tests/fixtures/typical.txt b/gix-mailmap/tests/fixtures/typical.txt index 563b9ddd90a..bbadc584836 100644 --- a/gix-mailmap/tests/fixtures/typical.txt +++ b/gix-mailmap/tests/fixtures/typical.txt @@ -6,3 +6,4 @@ Joe R. Developer Joe Jane Doe Jane Doe Jane Doe Jane + Jane diff --git a/gix-mailmap/tests/parse/mod.rs b/gix-mailmap/tests/parse/mod.rs index 28f33e4631e..0e7e38be9d5 100644 --- a/gix-mailmap/tests/parse/mod.rs +++ b/gix-mailmap/tests/parse/mod.rs @@ -31,6 +31,7 @@ fn a_typical_mailmap() { Entry::change_name_and_email_by_email("Jane Doe", "jane@example.com", "jane@laptop.(none)"), Entry::change_name_and_email_by_email("Jane Doe", "jane@example.com", "jane@desktop.(none)"), Entry::change_name_and_email_by_name_and_email("Jane Doe", "jane@example.com", "Jane", "bugs@example.com"), + Entry::change_email_by_name_and_email("jane@example.com", "Jane", "Jane@ipad.(none)"), ] ); } @@ -76,8 +77,8 @@ fn valid_entries() { Entry::change_name_and_email_by_email("proper name", "proper email", "commit-email") ); assert_eq!( - line(" proper name \tcommit name\t\t"), - Entry::change_name_and_email_by_name_and_email("proper name", "proper email", "commit name", "commit-email") + line(" commit name "), + Entry::change_email_by_name_and_email("proper-email", "commit name", "commit-email") ); } diff --git a/gix-mailmap/tests/snapshot/mod.rs b/gix-mailmap/tests/snapshot/mod.rs index 408ad49b083..316ebf722c4 100644 --- a/gix-mailmap/tests/snapshot/mod.rs +++ b/gix-mailmap/tests/snapshot/mod.rs @@ -31,6 +31,11 @@ fn try_resolve() { Some(signature("Jane Doe", "jane@example.com")), "name and email can be mapped specifically, case insensitive matching of name" ); + assert_eq!( + snapshot.resolve(signature("janE", "jane@ipad.(none)").to_ref()), + signature("janE", "jane@example.com"), + "an email can be mapped by name and email specifically, both match case-insensitively" + ); let sig = signature("Jane", "other@example.com"); assert_eq!(snapshot.try_resolve(sig.to_ref()), None, "unmatched email"); @@ -49,7 +54,7 @@ fn try_resolve() { ); assert_eq!(snapshot.resolve(sig.to_ref()), sig); - assert_eq!(snapshot.entries().len(), 5); + assert_eq!(snapshot.entries().len(), 6); } #[test] diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index b4c788952f1..e1aa5335e51 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -1216,6 +1216,17 @@ pub fn main() -> Result<()> { core::repository::mailmap::entries(repository(Mode::Lenient)?, format, out, err) }, ), + mailmap::Subcommands::Check { contacts } => prepare_and_run( + "mailmap-check", + trace, + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, err| { + core::repository::mailmap::check(repository(Mode::Lenient)?, format, contacts, out, err) + }, + ), }, Subcommands::Attributes(cmd) => match cmd { attributes::Subcommands::Query { statistics, pathspec } => prepare_and_run( diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index af1374241fb..2216da316d0 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -512,10 +512,17 @@ pub mod remote { } pub mod mailmap { + use gix::bstr::BString; + #[derive(Debug, clap::Subcommand)] pub enum Subcommands { /// Print all entries in configured mailmaps, inform about errors as well. Entries, + /// Print the canonical form of contacts according to the configured mailmaps. + Check { + /// One or more `Name ` or `` to pass through the mailmap. + contacts: Vec, + }, } }