mailmap/
lib.rs

1use std::fmt;
2use std::pin::Pin;
3use std::ptr::NonNull;
4
5#[cfg(test)]
6mod test;
7
8/// Loads a mailmap from the string passed in.
9///
10/// The format is the same as used by `git`; specifically:
11///
12/// * `Canonical Name <canonical email> Current Name <current email>`
13///   * This changes authors matching both name and email to the canonical forms.
14/// * `Canonical Name <current email>`
15///   * This changes all entries with this email to new name, regardless of their current name.
16/// * `Canonical Name <canonical email> <current email>`
17///   * This changes all entries with the current email to the canonical name and email.
18/// * `<canonical email> <current email>`
19///   * This changes all entries with the current email to the canonical email.
20pub struct Mailmap {
21    buffer: Pin<Box<str>>,
22    entries: Vec<RawMapEntry>,
23}
24
25impl fmt::Debug for Mailmap {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        let mut list = f.debug_list();
28        for entry in &self.entries {
29            // these entries were created from this buffer
30            let entry = unsafe { entry.to_entry(&self.buffer) };
31            list.entry(&entry);
32        }
33        list.finish()
34    }
35}
36
37#[derive(Copy, Clone)]
38struct RawMapEntry {
39    canonical_name: Option<NonNull<str>>,
40    canonical_email: Option<NonNull<str>>,
41    current_name: Option<NonNull<str>>,
42    current_email: Option<NonNull<str>>,
43}
44
45impl RawMapEntry {
46    unsafe fn to_entry<'a>(self, _: &'a str) -> MapEntry<'a> {
47        MapEntry {
48            canonical_name: self.canonical_name.map(|v| &*v.as_ptr()),
49            canonical_email: self.canonical_email.map(|v| &*v.as_ptr()),
50            current_name: self.current_name.map(|v| &*v.as_ptr()),
51            current_email: self.current_email.map(|v| &*v.as_ptr()),
52        }
53    }
54}
55
56#[derive(Copy, Clone, Debug, PartialEq, Eq)]
57struct MapEntry<'a> {
58    canonical_name: Option<&'a str>,
59    canonical_email: Option<&'a str>,
60    current_name: Option<&'a str>,
61    current_email: Option<&'a str>,
62}
63
64impl<'a> MapEntry<'a> {
65    fn to_raw_entry(self) -> RawMapEntry {
66        RawMapEntry {
67            canonical_name: self.canonical_name.map(|v| v.into()),
68            canonical_email: self.canonical_email.map(|v| v.into()),
69            current_name: self.current_name.map(|v| v.into()),
70            current_email: self.current_email.map(|v| v.into()),
71        }
72    }
73}
74
75#[derive(Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
76pub struct Author {
77    pub name: String,
78    pub email: String,
79}
80
81impl fmt::Debug for Author {
82    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83        write!(f, "{} <{}>", self.name, self.email)
84    }
85}
86
87impl Mailmap {
88    pub fn from_string(file: String) -> Result<Mailmap, Box<dyn std::error::Error>> {
89        let file = Pin::new(file.into_boxed_str());
90        let mut entries = Vec::with_capacity(file.lines().count());
91        for (idx, line) in file.lines().enumerate() {
92            if let Some(entry) = parse_line(&line, idx + 1) {
93                entries.push(entry.to_raw_entry());
94            }
95        }
96        Ok(Mailmap {
97            buffer: file,
98            entries,
99        })
100    }
101
102    pub fn canonicalize(&self, author: &Author) -> Author {
103        for entry in &self.entries {
104            // these entries were created from this buffer
105            let entry = unsafe { entry.to_entry(&self.buffer) };
106            if let Some(email) = entry.current_email {
107                if let Some(name) = entry.current_name {
108                    if author.name == name && author.email == email {
109                        return Author {
110                            name: entry.canonical_name.unwrap_or(&author.name).to_owned(),
111                            email: entry.canonical_email.expect("canonical email").to_owned(),
112                        };
113                    }
114                } else {
115                    if author.email == email {
116                        return Author {
117                            name: entry.canonical_name.unwrap_or(&author.name).to_owned(),
118                            email: entry.canonical_email.expect("canonical email").to_owned(),
119                        };
120                    }
121                }
122            }
123        }
124
125        author.clone()
126    }
127}
128
129fn read_email<'a>(line: &mut &'a str) -> Option<&'a str> {
130    if !line.starts_with('<') {
131        return None;
132    }
133
134    let end = line
135        .find('>')
136        .unwrap_or_else(|| panic!("could not find email end in {:?}", line));
137    let ret = &line[1..end];
138    *line = &line[end + 1..];
139    Some(ret)
140}
141
142fn read_name<'a>(line: &mut &'a str) -> Option<&'a str> {
143    let end = if let Some(end) = line.find('<') {
144        end
145    } else {
146        return None;
147    };
148    let ret = &line[..end].trim();
149    *line = &line[end..];
150    if ret.is_empty() {
151        None
152    } else {
153        Some(ret)
154    }
155}
156
157fn read_comment(line: &mut &str) -> bool {
158    if line.trim().starts_with('#') {
159        *line = "";
160        true
161    } else {
162        false
163    }
164}
165
166fn parse_line(mut line: &str, idx: usize) -> Option<MapEntry<'_>> {
167    let mut entry = MapEntry {
168        canonical_name: None,
169        canonical_email: None,
170        current_name: None,
171        current_email: None,
172    };
173    loop {
174        line = line.trim_start();
175        if read_comment(&mut line) || line.trim().is_empty() {
176            break;
177        }
178
179        if let Some(email) = read_email(&mut line) {
180            if entry.canonical_email.is_none() {
181                entry.canonical_email = Some(email);
182            } else {
183                if entry.current_email.is_some() {
184                    eprintln!("malformed mailmap on line {}: too many emails", idx);
185                } else {
186                    entry.current_email = Some(email);
187                }
188            }
189        } else if let Some(name) = read_name(&mut line) {
190            if entry.canonical_name.is_none() {
191                entry.canonical_name = Some(name);
192            } else {
193                if entry.current_name.is_some() {
194                    eprintln!("malformed mailmap on line {}: too many names", idx);
195                } else {
196                    entry.current_name = Some(name);
197                }
198            }
199        } else {
200            break;
201        }
202    }
203
204    if entry.canonical_email.is_some() && entry.current_email.is_none() {
205        entry.current_email = entry.canonical_email;
206    }
207
208    if entry.canonical_name.is_some()
209        || entry.canonical_email.is_some()
210        || entry.current_name.is_some()
211        || entry.current_email.is_some()
212    {
213        Some(entry)
214    } else {
215        None
216    }
217}