grit_lib/
commit_pretty.rs1use crate::objects::ObjectId;
4
5#[must_use]
12pub fn abbrev_hex(oid: &ObjectId, abbrev_len: usize) -> String {
13 let hex = oid.to_hex();
14 let n = abbrev_len.clamp(4, 40).min(hex.len());
15 hex[..n].to_owned()
16}
17
18#[must_use]
27pub fn message_subject(message: &str) -> String {
28 let mut subject_lines = Vec::new();
29 for line in MessageLines::new(message) {
30 if line.text.is_empty() {
31 if !subject_lines.is_empty() {
32 break;
33 }
34 continue;
35 }
36 subject_lines.push(line.text);
37 }
38 subject_lines.join(" ")
39}
40
41#[must_use]
51pub fn message_body(message: &str) -> &str {
52 let mut saw_subject = false;
53 let mut body_start = message.len();
54 let mut iter = MessageLines::new(message).peekable();
55
56 while let Some(line) = iter.next() {
57 if line.text.is_empty() {
58 if saw_subject {
59 body_start = line.next_start;
60 while let Some(next) = iter.peek() {
61 if !next.text.is_empty() {
62 break;
63 }
64 body_start = next.next_start;
65 iter.next();
66 }
67 break;
68 }
69 continue;
70 }
71 saw_subject = true;
72 }
73
74 &message[body_start..]
75}
76
77#[derive(Clone, Copy)]
78struct MessageLine<'a> {
79 text: &'a str,
80 next_start: usize,
81}
82
83struct MessageLines<'a> {
84 message: &'a str,
85 pos: usize,
86}
87
88impl<'a> MessageLines<'a> {
89 fn new(message: &'a str) -> Self {
90 Self { message, pos: 0 }
91 }
92}
93
94impl<'a> Iterator for MessageLines<'a> {
95 type Item = MessageLine<'a>;
96
97 fn next(&mut self) -> Option<Self::Item> {
98 if self.pos >= self.message.len() {
99 return None;
100 }
101 let start = self.pos;
102 let tail = &self.message[start..];
103 let newline_rel = tail.find('\n');
104 let (mut end, next_start) = match newline_rel {
105 Some(rel) => (start + rel, start + rel + 1),
106 None => (self.message.len(), self.message.len()),
107 };
108 if self.message.as_bytes().get(end.wrapping_sub(1)) == Some(&b'\r') && end > start {
109 end -= 1;
110 }
111 self.pos = next_start;
112 Some(MessageLine {
113 text: &self.message[start..end],
114 next_start,
115 })
116 }
117}
118
119fn parse_tz_offset_seconds(offset: &str) -> i64 {
120 if offset.len() < 5 {
121 return 0;
122 }
123 let sign = if offset.starts_with('-') { -1i64 } else { 1i64 };
124 let hours: i64 = offset[1..3].parse().unwrap_or(0);
125 let minutes: i64 = offset[3..5].parse().unwrap_or(0);
126 sign * (hours * 3600 + minutes * 60)
127}
128
129#[must_use]
133pub fn format_short_date_from_ident(ident: &str) -> String {
134 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
135 if parts.len() < 2 {
136 return ident.to_owned();
137 }
138 let ts_str = parts[1];
139 let offset_str = parts[0];
140 let Ok(ts) = ts_str.parse::<i64>() else {
141 return ident.to_owned();
142 };
143 let offset_secs = parse_tz_offset_seconds(offset_str);
144 let Ok(dt) = time::OffsetDateTime::from_unix_timestamp(ts + offset_secs) else {
145 return ident.to_owned();
146 };
147 let format = time::format_description::parse("[year]-[month]-[day]");
148 let Ok(fmt) = format else {
149 return ident.to_owned();
150 };
151 dt.format(&fmt).unwrap_or_else(|_| ident.to_owned())
152}
153
154#[must_use]
164pub fn format_reference_line(
165 oid: &ObjectId,
166 subject_first_line: &str,
167 committer_ident: &str,
168 abbrev_len: usize,
169) -> String {
170 let abbrev = abbrev_hex(oid, abbrev_len);
171 let date = format_short_date_from_ident(committer_ident);
172 format!("{abbrev} ({subject_first_line}, {date})")
173}
174
175#[allow(clippy::unwrap_used)]
188#[must_use]
189pub fn add_wrapped_text(text: &str, indent1: i64, indent2: i64, width: i64) -> String {
190 use unicode_width::UnicodeWidthChar;
191
192 if width <= 0 {
193 return add_indented_text(text, indent1, indent2);
194 }
195
196 let bytes = text.as_bytes();
197 let mut out = String::new();
198
199 let mut bol: usize = 0;
203 let mut space: Option<usize> = None;
204 let mut indent: i64 = indent1;
205 let mut w: i64 = indent1;
206 if indent1 < 0 {
207 w = -indent1;
208 space = Some(0);
209 }
210
211 let mut text_pos: usize = 0;
212 loop {
213 let c = bytes.get(text_pos).copied();
214 let is_space = is_space_byte(c);
215 if c.is_none() || is_space {
216 let mut do_new_line = false;
217 if w <= width || space.is_none() {
218 let start = if let Some(sp) = space {
219 sp
220 } else {
221 if c.is_none() && text_pos == bol {
222 return out;
223 }
224 for _ in 0..indent.max(0) {
225 out.push(' ');
226 }
227 bol
228 };
229 out.push_str(&text[start..text_pos]);
230 if c.is_none() {
231 return out;
232 }
233 let cc = c.unwrap();
234 let mut new_space = text_pos;
235 if cc == b'\t' {
236 w |= 0x07;
237 } else if cc == b'\n' {
238 new_space += 1;
239 match bytes.get(new_space) {
240 Some(b'\n') => {
241 out.push('\n');
242 space = Some(new_space);
243 do_new_line = true;
244 }
245 nxt if !nxt.map(u8::is_ascii_alphanumeric).unwrap_or(false) => {
246 space = Some(new_space);
247 do_new_line = true;
248 }
249 _ => {
250 out.push(' ');
251 }
252 }
253 }
254 if !do_new_line {
255 space = Some(new_space);
256 w += 1;
257 text_pos += 1;
258 }
259 } else {
260 do_new_line = true;
261 }
262 if do_new_line {
263 out.push('\n');
264 let sp = space.unwrap();
265 text_pos = sp + usize::from(is_space_byte(bytes.get(sp).copied()));
266 bol = text_pos;
267 space = None;
268 w = indent2;
269 indent = indent2;
270 }
271 continue;
272 }
273 let ch = text[text_pos..].chars().next().unwrap();
275 let gw = UnicodeWidthChar::width(ch).unwrap_or(0) as i64;
276 w += gw;
277 text_pos += ch.len_utf8();
278 }
279}
280
281fn is_space_byte(b: Option<u8>) -> bool {
282 matches!(
283 b,
284 Some(b' ') | Some(b'\t') | Some(b'\n') | Some(b'\r') | Some(11) | Some(12)
285 )
286}
287
288#[must_use]
291pub fn add_indented_text(text: &str, indent1: i64, indent2: i64) -> String {
292 let indent1 = indent1.max(0);
293 let mut out = String::new();
294 let mut indent = indent1;
295 let bytes = text.as_bytes();
296 let mut pos = 0;
297 while pos < bytes.len() {
298 let eol = match bytes[pos..].iter().position(|&b| b == b'\n') {
299 Some(i) => pos + i + 1,
300 None => bytes.len(),
301 };
302 for _ in 0..indent {
303 out.push(' ');
304 }
305 out.push_str(&text[pos..eol]);
306 pos = eol;
307 indent = indent2;
308 }
309 out
310}
311
312#[cfg(test)]
313mod wrap_tests {
314 use super::add_wrapped_text;
315
316 #[test]
317 fn wrap_width_one_decoration_with_leading_newline() {
318 let input = "\n (tag: describe-me)%+w(2)";
321 assert_eq!(
322 add_wrapped_text(input, 0, 0, 1),
323 "\n(tag:\ndescribe-me)%+w(2)"
324 );
325 }
326
327 #[test]
328 fn wrap_zero_width_is_indent_only() {
329 assert_eq!(add_wrapped_text("a\nb", 2, 1, 0), " a\n b");
330 }
331
332 #[test]
333 fn wrap_simple_words() {
334 assert_eq!(add_wrapped_text("foo bar", 0, 0, 80), "foo bar");
336 }
337}