Skip to content

Commit 3820a0f

Browse files
authored
Merge pull request #2030 from ralphmodales/trailer-conviences
add convenience methods for common Git trailers
2 parents b199c6e + 6c6dfd0 commit 3820a0f

File tree

2 files changed

+98
-5
lines changed

2 files changed

+98
-5
lines changed

gix-object/src/commit/message/body.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ pub struct Trailers<'a> {
2323
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
2424
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2525
pub struct TrailerRef<'a> {
26-
/// The name of the trailer, like "Signed-off-by", up to the separator ": "
26+
/// The name of the trailer, like "Signed-off-by", up to the separator `: `.
2727
#[cfg_attr(feature = "serde", serde(borrow))]
2828
pub token: &'a BStr,
29-
/// The value right after the separator ": ", with leading and trailing whitespace trimmed.
29+
/// The value right after the separator `: `, with leading and trailing whitespace trimmed.
3030
/// Note that multi-line values aren't currently supported.
3131
pub value: &'a BStr,
3232
}
@@ -93,7 +93,7 @@ impl<'a> BodyRef<'a> {
9393
self.body_without_trailer
9494
}
9595

96-
/// Return an iterator over the trailers parsed from the last paragraph of the body. May be empty.
96+
/// Return an iterator over the trailers parsed from the last paragraph of the body. Maybe empty.
9797
pub fn trailers(&self) -> Trailers<'a> {
9898
Trailers {
9999
cursor: self.start_of_trailer,
@@ -114,6 +114,69 @@ impl Deref for BodyRef<'_> {
114114
self.body_without_trailer
115115
}
116116
}
117+
118+
/// Convenience methods
119+
impl TrailerRef<'_> {
120+
/// Check if this trailer is a `Signed-off-by` trailer (case-insensitive).
121+
pub fn is_signed_off_by(&self) -> bool {
122+
self.token.eq_ignore_ascii_case(b"Signed-off-by")
123+
}
124+
125+
/// Check if this trailer is a `Co-authored-by` trailer (case-insensitive).
126+
pub fn is_co_authored_by(&self) -> bool {
127+
self.token.eq_ignore_ascii_case(b"Co-authored-by")
128+
}
129+
130+
/// Check if this trailer is an `Acked-by` trailer (case-insensitive).
131+
pub fn is_acked_by(&self) -> bool {
132+
self.token.eq_ignore_ascii_case(b"Acked-by")
133+
}
134+
135+
/// Check if this trailer is a `Reviewed-by` trailer (case-insensitive).
136+
pub fn is_reviewed_by(&self) -> bool {
137+
self.token.eq_ignore_ascii_case(b"Reviewed-by")
138+
}
139+
140+
/// Check if this trailer is a `Tested-by` trailer (case-insensitive).
141+
pub fn is_tested_by(&self) -> bool {
142+
self.token.eq_ignore_ascii_case(b"Tested-by")
143+
}
144+
145+
/// Check if this trailer represents any kind of authorship or attribution
146+
/// (`Signed-off-by`, `Co-authored-by`, etc.).
147+
pub fn is_attribution(&self) -> bool {
148+
self.is_signed_off_by()
149+
|| self.is_co_authored_by()
150+
|| self.is_acked_by()
151+
|| self.is_reviewed_by()
152+
|| self.is_tested_by()
153+
}
154+
}
155+
156+
/// Convenience methods
157+
impl<'a> Trailers<'a> {
158+
/// Filter trailers to only include `Signed-off-by` entries.
159+
pub fn signed_off_by(self) -> impl Iterator<Item = TrailerRef<'a>> {
160+
self.filter(TrailerRef::is_signed_off_by)
161+
}
162+
163+
/// Filter trailers to only include `Co-authored-by` entries.
164+
pub fn co_authored_by(self) -> impl Iterator<Item = TrailerRef<'a>> {
165+
self.filter(TrailerRef::is_co_authored_by)
166+
}
167+
168+
/// Filter trailers to only include attribution-related entries.
169+
/// (`Signed-off-by`, `Co-authored-by`, `Acked-by`, `Reviewed-by`, `Tested-by`).
170+
pub fn attributions(self) -> impl Iterator<Item = TrailerRef<'a>> {
171+
self.filter(TrailerRef::is_attribution)
172+
}
173+
174+
/// Filter trailers to only include authors from `Signed-off-by` and `Co-authored-by` entries.
175+
pub fn authors(self) -> impl Iterator<Item = TrailerRef<'a>> {
176+
self.filter(|trailer| trailer.is_signed_off_by() || trailer.is_co_authored_by())
177+
}
178+
}
179+
117180
#[cfg(test)]
118181
mod test_parse_trailer {
119182
use super::*;

gix-object/src/commit/message/mod.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,44 @@ impl<'a> CommitRef<'a> {
1717
}
1818

1919
/// Return an iterator over message trailers as obtained from the last paragraph of the commit message.
20-
/// May be empty.
20+
/// Maybe empty.
2121
pub fn message_trailers(&self) -> body::Trailers<'a> {
2222
BodyRef::from_bytes(self.message).trailers()
2323
}
2424
}
2525

26+
/// Convenience methods
27+
impl<'a> CommitRef<'a> {
28+
/// Get an iterator over all `Signed-off-by` trailers in the commit message.
29+
/// This is useful for finding who signed off on the commit.
30+
pub fn signed_off_by_trailers(&self) -> impl Iterator<Item = body::TrailerRef<'a>> {
31+
self.message_trailers().signed_off_by()
32+
}
33+
34+
/// Get an iterator over `Co-authored-by` trailers in the commit message.
35+
/// This is useful for squashed commits that contain multiple authors.
36+
pub fn co_authored_by_trailers(&self) -> impl Iterator<Item = body::TrailerRef<'a>> {
37+
self.message_trailers().co_authored_by()
38+
}
39+
40+
/// Get all authors mentioned in `Signed-off-by` and `Co-authored-by` trailers.
41+
/// This is useful for squashed commits that contain multiple authors.
42+
/// Returns a Vec of author strings that can include both signers and co-authors.
43+
pub fn author_trailers(&self) -> impl Iterator<Item = body::TrailerRef<'a>> {
44+
self.message_trailers().authors()
45+
}
46+
47+
/// Get an iterator over all attribution-related trailers
48+
/// (`Signed-off-by,` `Co-authored-by`, `Acked-by`, `Reviewed-by`, `Tested-by`).
49+
/// This provides a comprehensive view of everyone who contributed to or reviewed the commit.
50+
/// Note that the same name may occur multiple times, it's not a unified list.
51+
pub fn attribution_trailers(&self) -> impl Iterator<Item = body::TrailerRef<'a>> {
52+
self.message_trailers().attributions()
53+
}
54+
}
55+
2656
impl<'a> MessageRef<'a> {
27-
/// Parse the given `input` as message.
57+
/// Parse the given `input` as a message.
2858
///
2959
/// Note that this cannot fail as everything will be interpreted as title if there is no body separator.
3060
pub fn from_bytes(input: &'a [u8]) -> Self {

0 commit comments

Comments
 (0)