radicle_git_ext/commit/
trailers.rs

1use std::{borrow::Cow, fmt, fmt::Write, ops::Deref, str::FromStr};
2
3use git2::{MessageTrailersStrs, MessageTrailersStrsIterator};
4
5/// A Git commit's set of trailers that are left in the commit's
6/// message.
7///
8/// Trailers are key/value pairs in the last paragraph of a message,
9/// not including any patches or conflicts that may be present.
10///
11/// # Usage
12///
13/// To construct `Trailers`, you can use [`Trailers::parse`] or its
14/// `FromStr` implementation.
15///
16/// To iterate over the trailers, you can use [`Trailers::iter`].
17///
18/// To render the trailers to a `String`, you can use
19/// [`Trailers::to_string`] or its `Display` implementation (note that
20/// it will default to using `": "` as the separator.
21///
22/// # Examples
23///
24/// ```text
25/// Add new functionality
26///
27/// Making code better with new functionality.
28///
29/// X-Signed-Off-By: Alex Sellier
30/// X-Co-Authored-By: Fintan Halpenny
31/// ```
32///
33/// The trailers in the above example are:
34///
35/// ```text
36/// X-Signed-Off-By: Alex Sellier
37/// X-Co-Authored-By: Fintan Halpenny
38/// ```
39pub struct Trailers {
40    inner: MessageTrailersStrs,
41}
42
43impl Trailers {
44    pub fn parse(message: &str) -> Result<Self, git2::Error> {
45        Ok(Self {
46            inner: git2::message_trailers_strs(message)?,
47        })
48    }
49
50    pub fn iter(&self) -> Iter<'_> {
51        Iter {
52            inner: self.inner.iter(),
53        }
54    }
55
56    pub fn to_string<'a, S>(&self, sep: S) -> String
57    where
58        S: Separator<'a>,
59    {
60        let mut buf = String::new();
61        for (i, trailer) in self.iter().enumerate() {
62            if i > 0 {
63                writeln!(buf).ok();
64            }
65
66            write!(buf, "{}", trailer.display(sep.sep_for(&trailer.token))).ok();
67        }
68        writeln!(buf).ok();
69        buf
70    }
71}
72
73impl fmt::Display for Trailers {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str(&self.to_string(": "))
76    }
77}
78
79pub trait Separator<'a> {
80    fn sep_for(&self, token: &Token) -> &'a str;
81}
82
83impl<'a> Separator<'a> for &'a str {
84    fn sep_for(&self, _: &Token) -> &'a str {
85        self
86    }
87}
88
89impl<'a, F> Separator<'a> for F
90where
91    F: Fn(&Token) -> &'a str,
92{
93    fn sep_for(&self, token: &Token) -> &'a str {
94        self(token)
95    }
96}
97
98impl FromStr for Trailers {
99    type Err = git2::Error;
100
101    fn from_str(s: &str) -> Result<Self, Self::Err> {
102        Self::parse(s)
103    }
104}
105
106pub struct Iter<'a> {
107    inner: MessageTrailersStrsIterator<'a>,
108}
109
110impl<'a> Iterator for Iter<'a> {
111    type Item = Trailer<'a>;
112
113    fn next(&mut self) -> Option<Self::Item> {
114        let (token, value) = self.inner.next()?;
115        Some(Trailer {
116            token: Token(token),
117            value: Cow::Borrowed(value),
118        })
119    }
120}
121
122#[derive(Debug, Clone, Eq, PartialEq)]
123pub struct Token<'a>(&'a str);
124
125impl Deref for Token<'_> {
126    type Target = str;
127
128    fn deref(&self) -> &Self::Target {
129        self.0
130    }
131}
132
133impl<'a> TryFrom<&'a str> for Token<'a> {
134    type Error = &'static str;
135
136    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
137        let is_token = s.chars().all(|c| c.is_alphanumeric() || c == '-');
138        if is_token {
139            Ok(Token(s))
140        } else {
141            Err("token contains invalid characters")
142        }
143    }
144}
145
146pub struct Display<'a> {
147    trailer: &'a Trailer<'a>,
148    separator: &'a str,
149}
150
151impl fmt::Display for Display<'_> {
152    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
153        write!(
154            f,
155            "{}{}{}",
156            self.trailer.token.deref(),
157            self.separator,
158            self.trailer.value,
159        )
160    }
161}
162
163/// A trailer is a key/value pair found in the last paragraph of a Git
164/// commit message, not including any patches or conflicts that may be
165/// present.
166#[derive(Debug, Clone, Eq, PartialEq)]
167pub struct Trailer<'a> {
168    pub token: Token<'a>,
169    pub value: Cow<'a, str>,
170}
171
172impl<'a> Trailer<'a> {
173    pub fn display(&'a self, separator: &'a str) -> Display<'a> {
174        Display {
175            trailer: self,
176            separator,
177        }
178    }
179
180    pub fn to_owned(&self) -> OwnedTrailer {
181        OwnedTrailer::from(self)
182    }
183}
184
185/// A version of the [`Trailer`] which owns its token and
186/// value. Useful for when you need to carry trailers around in a long
187/// lived data structure.
188#[derive(Debug)]
189pub struct OwnedTrailer {
190    pub token: OwnedToken,
191    pub value: String,
192}
193
194#[derive(Debug)]
195pub struct OwnedToken(String);
196
197impl Deref for OwnedToken {
198    type Target = str;
199
200    fn deref(&self) -> &Self::Target {
201        &self.0
202    }
203}
204
205impl<'a> From<&Trailer<'a>> for OwnedTrailer {
206    fn from(t: &Trailer<'a>) -> Self {
207        OwnedTrailer {
208            token: OwnedToken(t.token.0.to_string()),
209            value: t.value.to_string(),
210        }
211    }
212}
213
214impl<'a> From<Trailer<'a>> for OwnedTrailer {
215    fn from(t: Trailer<'a>) -> Self {
216        (&t).into()
217    }
218}
219
220impl<'a> From<&'a OwnedTrailer> for Trailer<'a> {
221    fn from(t: &'a OwnedTrailer) -> Self {
222        Trailer {
223            token: Token(t.token.0.as_str()),
224            value: Cow::from(&t.value),
225        }
226    }
227}