Skip to content

Commit 1714d05

Browse files
committed
feat: CommitRef::summary() and MessageRef::body() methods (#198)
1 parent 7055dc8 commit 1714d05

File tree

3 files changed

+75
-27
lines changed

3 files changed

+75
-27
lines changed

git-object/src/commit/message.rs

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
bstr::{BStr, BString, ByteSlice, ByteVec},
55
commit::MessageRef,
66
};
7+
use std::ops::Deref;
78

89
mod decode {
910
use nom::{
@@ -75,6 +76,7 @@ impl<'a> MessageRef<'a> {
7576
let (title, body) = decode::bytes(input);
7677
MessageRef { title, body }
7778
}
79+
7880
/// Produce a short commit summary for the message title.
7981
///
8082
/// This means the following
@@ -85,38 +87,73 @@ impl<'a> MessageRef<'a> {
8587
/// The resulting summary will have folded whitespace before a newline into spaces and stopped that process
8688
/// once two consecutive newlines are encountered.
8789
pub fn summary(&self) -> Cow<'a, BStr> {
88-
let message = self.title.trim();
89-
match message.find_byte(b'\n') {
90-
Some(mut pos) => {
91-
let mut out = BString::default();
92-
let mut previous_pos = None;
93-
loop {
94-
if let Some(previous_pos) = previous_pos {
95-
if previous_pos + 1 == pos {
96-
let len_after_trim = out.trim_end().len();
97-
out.resize(len_after_trim, 0);
98-
break out.into();
99-
}
100-
}
101-
let message_to_newline = &message[previous_pos.map(|p| p + 1).unwrap_or(0)..pos];
90+
summary(self.title)
91+
}
92+
93+
/// Further parse the body into into non-trailer and trailers, which can be iterated from the returned [`BodyRef`].
94+
pub fn body(&self) -> Option<BodyRef<'a>> {
95+
self.body.map(|b| BodyRef {
96+
_body_without_footer: b,
97+
_start_of_footer: &[],
98+
})
99+
}
100+
}
102101

103-
if let Some(pos_before_whitespace) = message_to_newline.rfind_not_byteset(b"\t\n\x0C\r ") {
104-
out.extend_from_slice(&message_to_newline[..pos_before_whitespace + 1]);
102+
pub fn summary(message: &BStr) -> Cow<'_, BStr> {
103+
let message = message.trim();
104+
match message.find_byte(b'\n') {
105+
Some(mut pos) => {
106+
let mut out = BString::default();
107+
let mut previous_pos = None;
108+
loop {
109+
if let Some(previous_pos) = previous_pos {
110+
if previous_pos + 1 == pos {
111+
let len_after_trim = out.trim_end().len();
112+
out.resize(len_after_trim, 0);
113+
break out.into();
105114
}
106-
out.push_byte(b' ');
107-
previous_pos = Some(pos);
108-
match message.get(pos + 1..).and_then(|i| i.find_byte(b'\n')) {
109-
Some(next_nl_pos) => pos += next_nl_pos + 1,
110-
None => {
111-
if let Some(slice) = message.get((pos + 1)..) {
112-
out.extend_from_slice(slice);
113-
}
114-
break out.into();
115+
}
116+
let message_to_newline = &message[previous_pos.map(|p| p + 1).unwrap_or(0)..pos];
117+
118+
if let Some(pos_before_whitespace) = message_to_newline.rfind_not_byteset(b"\t\n\x0C\r ") {
119+
out.extend_from_slice(&message_to_newline[..pos_before_whitespace + 1]);
120+
}
121+
out.push_byte(b' ');
122+
previous_pos = Some(pos);
123+
match message.get(pos + 1..).and_then(|i| i.find_byte(b'\n')) {
124+
Some(next_nl_pos) => pos += next_nl_pos + 1,
125+
None => {
126+
if let Some(slice) = message.get((pos + 1)..) {
127+
out.extend_from_slice(slice);
115128
}
129+
break out.into();
116130
}
117131
}
118132
}
119-
None => message.as_bstr().into(),
120133
}
134+
None => message.as_bstr().into(),
135+
}
136+
}
137+
138+
/// A reference to a message body, further parsed to only contain the non-trailer parts.
139+
///
140+
/// See [git-interpret-trailers](https://git-scm.com/docs/git-interpret-trailers) for more information
141+
/// on what constitutes trailers and not that this implementation is only good for typical sign-off footer or key-value parsing.
142+
pub struct BodyRef<'a> {
143+
_body_without_footer: &'a BStr,
144+
_start_of_footer: &'a [u8],
145+
}
146+
147+
impl<'a> AsRef<BStr> for BodyRef<'a> {
148+
fn as_ref(&self) -> &BStr {
149+
self._body_without_footer
150+
}
151+
}
152+
153+
impl<'a> Deref for BodyRef<'a> {
154+
type Target = BStr;
155+
156+
fn deref(&self) -> &Self::Target {
157+
self._body_without_footer
121158
}
122159
}

git-object/src/commit/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use bstr::{BStr, ByteSlice};
22

33
use crate::{Commit, CommitRef, TagRef};
4+
use std::borrow::Cow;
45

56
mod decode;
67
mod message;
@@ -51,6 +52,11 @@ impl<'a> CommitRef<'a> {
5152
pub fn message(&self) -> MessageRef<'a> {
5253
MessageRef::from_bytes(self.message)
5354
}
55+
56+
/// Return exactly the same message as [`MessageRef::summary()`].
57+
pub fn summary(&self) -> Cow<'a, BStr> {
58+
message::summary(self.message)
59+
}
5460
}
5561

5662
impl Commit {

git-object/tests/immutable/commit/from_bytes.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,12 @@ Signed-off-by: Kim Altintop <kim@eagain.st>"
184184
.into()
185185
)
186186
);
187-
// let body = message.body();
187+
assert_eq!(
188+
commit.summary(),
189+
message.summary(),
190+
"both summaries are the same, but the commit one does less parsing"
191+
);
192+
let _body = message.body();
188193
Ok(())
189194
}
190195

0 commit comments

Comments
 (0)