1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! Canonical names and email addresses for presenting commit identities.
//!
//! A mailmap changes presentation without changing stored signatures or object IDs. Parse one
//! source with [`Mailmap::parse`], append higher-precedence sources with [`Mailmap::extend`], then
//! resolve author or committer bytes with [`Mailmap::resolve`]. Unknown identities are borrowed
//! unchanged. Matching uses ASCII case folding for both names and emails; output preserves bytes.
//!
//! ```
//! use girt::mailmap::Mailmap;
//! let map = Mailmap::parse(b"A. Writer <writer@example.com> <old@example.com>\n");
//! let identity = map.resolve(b"Writer", b"OLD@example.com");
//! assert_eq!(identity.name, b"A. Writer");
//! assert_eq!(identity.email, b"writer@example.com");
//! ```
mod load;
pub use load::Error;
/// An identity selected for display, borrowing either the input or the map.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Identity<'a> {
/// Display name, without encoding conversion.
pub name: &'a [u8],
/// Display email address, without angle brackets.
pub email: &'a [u8],
}
/// Parsed mailmap entries in source order.
///
/// All four Git line forms are accepted. Blank, comment and malformed lines are ignored.
/// Later entries replace only the canonical components they supply. A matching name-specific
/// entry takes precedence over an email-only entry, regardless of source order.
#[derive(Clone, Debug, Default)]
pub struct Mailmap {
entries: Vec<Entry>,
}
#[derive(Clone, Debug)]
struct Entry {
email: Vec<u8>,
name: Option<Vec<u8>>,
canonical_name: Option<Vec<u8>>,
canonical_email: Option<Vec<u8>>,
}
impl Mailmap {
/// Parses mailmap bytes. Invalid lines are ignored as by `git check-mailmap`.
pub fn parse(bytes: &[u8]) -> Self {
let mut map = Self::default();
map.extend(bytes);
map
}
/// Adds another source, overriding earlier supplied components for the same lookup key.
pub fn extend(&mut self, bytes: &[u8]) {
for line in bytes.split(|&b| b == b'\n') {
if let Some(entry) = parse_line(line) {
self.entries.push(entry);
}
}
}
/// Maps an imported signature for display, preserving its exact date bytes.
pub fn resolve_signature<'a>(
&'a self,
signature: crate::IdentityRef<'a>,
) -> crate::IdentityRef<'a> {
let identity = self.resolve(signature.name, signature.email);
crate::IdentityRef {
name: identity.name,
email: identity.email,
date_bytes: signature.date_bytes,
}
}
/// Resolves one name/email pair without modifying its stored signature.
///
/// Name-specific matches use the original name. Resolution is a single lookup, not recursive.
pub fn resolve<'a>(&'a self, name: &'a [u8], email: &'a [u8]) -> Identity<'a> {
let specific = self.entries.iter().any(|entry| {
entry.email.eq_ignore_ascii_case(email)
&& entry
.name
.as_deref()
.is_some_and(|n| n.eq_ignore_ascii_case(name))
});
let mut result = Identity { name, email };
for entry in &self.entries {
if !entry.email.eq_ignore_ascii_case(email) {
continue;
}
let matches = match entry.name.as_deref() {
Some(n) => specific && n.eq_ignore_ascii_case(name),
None => !specific,
};
if matches {
if let Some(name) = &entry.canonical_name {
result.name = name;
}
if let Some(email) = &entry.canonical_email {
result.email = email;
}
}
}
result
}
}
fn parse_line(line: &[u8]) -> Option<Entry> {
let line = line.trim_ascii();
if line.starts_with(b"#") {
return None;
}
let (canonical_name, canonical_email, tail) = identity(line)?;
let canonical_name = (!canonical_name.is_empty()).then(|| canonical_name.to_vec());
if let Some((name, email, _)) = identity(tail) {
Some(Entry {
email: email.to_vec(),
name: (!name.is_empty()).then(|| name.to_vec()),
canonical_name,
canonical_email: Some(canonical_email.to_vec()),
})
} else {
Some(Entry {
email: canonical_email.to_vec(),
name: None,
canonical_name,
canonical_email: None,
})
}
}
fn identity(bytes: &[u8]) -> Option<(&[u8], &[u8], &[u8])> {
let open = bytes.iter().position(|&b| b == b'<')?;
let end = open + 1 + bytes[open + 1..].iter().position(|&b| b == b'>')?;
Some((
bytes[..open].trim_ascii(),
&bytes[open + 1..end],
&bytes[end + 1..],
))
}