gix_mailmap/snapshot/
mod.rs

1use bstr::ByteSlice;
2use gix_actor::SignatureRef;
3
4use crate::Snapshot;
5
6mod signature;
7pub use signature::{ResolvedSignature, Signature};
8
9mod util;
10use util::EncodedStringRef;
11
12mod entry;
13pub(crate) use entry::EmailEntry;
14
15impl Snapshot {
16    /// Create a new snapshot from the given bytes buffer, ignoring all parse errors that may occur on a line-by-line basis.
17    ///
18    /// This is similar to what git does.
19    pub fn from_bytes(buf: &[u8]) -> Self {
20        Self::new(crate::parse_ignore_errors(buf))
21    }
22
23    /// Create a new instance from `entries`.
24    ///
25    /// These can be obtained using [`crate::parse()`].
26    pub fn new<'a>(entries: impl IntoIterator<Item = crate::Entry<'a>>) -> Self {
27        let mut snapshot = Self::default();
28        snapshot.merge(entries);
29        snapshot
30    }
31
32    /// Merge the given `entries` into this instance, possibly overwriting existing mappings with
33    /// new ones should they collide.
34    pub fn merge<'a>(&mut self, entries: impl IntoIterator<Item = crate::Entry<'a>>) -> &mut Self {
35        for entry in entries {
36            let old_email: EncodedStringRef<'_> = entry.old_email.into();
37            assert!(
38                entry.new_name.is_some() || entry.new_email.is_some(),
39                "BUG: encountered entry without any mapped/new name or email."
40            );
41            match self
42                .entries_by_old_email
43                .binary_search_by(|e| e.old_email.cmp_ref(old_email))
44            {
45                Ok(pos) => self.entries_by_old_email[pos].merge(entry),
46                Err(insert_pos) => {
47                    self.entries_by_old_email.insert(insert_pos, entry.into());
48                }
49            };
50        }
51        self
52    }
53
54    /// Transform our acceleration structure into an iterator of entries.
55    ///
56    /// Note that the order is different from how they were obtained initially, and are explicitly ordered by
57    /// (`old_email`, `old_name`).
58    pub fn iter(&self) -> impl Iterator<Item = crate::Entry<'_>> {
59        self.entries_by_old_email.iter().flat_map(|entry| {
60            let initial = if entry.new_email.is_some() || entry.new_name.is_some() {
61                Some(crate::Entry {
62                    new_name: entry.new_name.as_ref().map(|b| b.as_bstr()),
63                    new_email: entry.new_email.as_ref().map(|b| b.as_bstr()),
64                    old_name: None,
65                    old_email: entry.old_email.as_bstr(),
66                })
67            } else {
68                None
69            };
70
71            let rest = entry.entries_by_old_name.iter().map(|name_entry| crate::Entry {
72                new_name: name_entry.new_name.as_ref().map(|b| b.as_bstr()),
73                new_email: name_entry.new_email.as_ref().map(|b| b.as_bstr()),
74                old_name: name_entry.old_name.as_bstr().into(),
75                old_email: entry.old_email.as_bstr(),
76            });
77
78            initial.into_iter().chain(rest)
79        })
80    }
81
82    /// Transform our acceleration structure into a list of entries.
83    ///
84    /// Note that the order is different from how they were obtained initially, and are explicitly ordered by
85    /// (`old_email`, `old_name`).
86    pub fn entries(&self) -> Vec<crate::Entry<'_>> {
87        self.iter().collect()
88    }
89
90    /// Try to resolve `signature` by its contained email and name and provide resolved/mapped names as reference.
91    /// Return `None` if no such mapping was found.
92    ///
93    /// Note that opposed to what git seems to do, we also normalize the case of email addresses to match the one
94    /// given in the mailmap. That is, if `Alex@example.com` is the current email, it will be matched and replaced with
95    /// `alex@example.com`. This leads to better mapping results and saves entries in the mailmap.
96    ///
97    /// This is the fastest possible lookup as there is no allocation.
98    pub fn try_resolve_ref(&self, signature: gix_actor::SignatureRef<'_>) -> Option<ResolvedSignature<'_>> {
99        let email: EncodedStringRef<'_> = signature.email.into();
100        let pos = self
101            .entries_by_old_email
102            .binary_search_by(|e| e.old_email.cmp_ref(email))
103            .ok()?;
104        let entry = &self.entries_by_old_email[pos];
105
106        let name: EncodedStringRef<'_> = signature.name.into();
107
108        match entry.entries_by_old_name.binary_search_by(|e| e.old_name.cmp_ref(name)) {
109            Ok(pos) => {
110                let name_entry = &entry.entries_by_old_name[pos];
111                ResolvedSignature::try_new(
112                    name_entry.new_email.as_ref(),
113                    entry.old_email.as_bstr(),
114                    signature.email,
115                    name_entry.new_name.as_ref(),
116                )
117            }
118            Err(_) => ResolvedSignature::try_new(
119                entry.new_email.as_ref(),
120                entry.old_email.as_bstr(),
121                signature.email,
122                entry.new_name.as_ref(),
123            ),
124        }
125    }
126
127    /// Try to resolve `signature` by its contained email and name and provide resolved/mapped names as owned signature,
128    /// with the mapped name and/or email replaced accordingly.
129    ///
130    /// Return `None` if no such mapping was found.
131    pub fn try_resolve(&self, signature: gix_actor::SignatureRef<'_>) -> Option<gix_actor::Signature> {
132        self.try_resolve_ref(signature)
133            .map(|new| enriched_signature(signature, new).into())
134    }
135
136    /// Like [`try_resolve()`][Snapshot::try_resolve()], but always returns an owned signature, which might be a copy
137    /// of `signature` if no mapping was found.
138    ///
139    /// Note that this method will always allocate.
140    pub fn resolve(&self, signature: gix_actor::SignatureRef<'_>) -> gix_actor::Signature {
141        self.try_resolve(signature).unwrap_or_else(|| signature.to_owned())
142    }
143
144    /// Like [`try_resolve()`][Snapshot::try_resolve()], but always returns a special copy-on-write signature, which contains
145    /// changed names or emails as `Cow::Owned`, or `Cow::Borrowed` if no mapping was found.
146    pub fn resolve_cow<'a>(&self, signature: gix_actor::SignatureRef<'a>) -> Signature<'a> {
147        self.try_resolve_ref(signature)
148            .map_or_else(|| signature.into(), |new| enriched_signature(signature, new))
149    }
150}
151
152fn enriched_signature<'a>(
153    SignatureRef { name, email, time }: SignatureRef<'a>,
154    new: ResolvedSignature<'_>,
155) -> Signature<'a> {
156    match (new.email, new.name) {
157        (Some(new_email), Some(new_name)) => Signature {
158            email: new_email.to_owned().into(),
159            name: new_name.to_owned().into(),
160            time,
161        },
162        (Some(new_email), None) => Signature {
163            email: new_email.to_owned().into(),
164            name: name.into(),
165            time,
166        },
167        (None, Some(new_name)) => Signature {
168            email: email.into(),
169            name: new_name.to_owned().into(),
170            time,
171        },
172        (None, None) => unreachable!("BUG: ResolvedSignatures don't exist here when nothing is set"),
173    }
174}