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#[must_use]
185pub fn add_wrapped_text(text: &str, indent1: i64, indent2: i64, width: i64) -> String {
186 use unicode_width::UnicodeWidthChar;
187
188 if width <= 0 {
189 return add_indented_text(text, indent1, indent2);
190 }
191
192 let bytes = text.as_bytes();
193 let mut out = String::new();
194
195 let mut bol: usize = 0;
199 let mut space: Option<usize> = None;
200 let mut indent: i64 = indent1;
201 let mut w: i64 = indent1;
202 if indent1 < 0 {
203 w = -indent1;
204 space = Some(0);
205 }
206
207 let mut text_pos: usize = 0;
208 loop {
209 let c = bytes.get(text_pos).copied();
210 let is_space = is_space_byte(c);
211 if c.is_none() || is_space {
212 let mut do_new_line = false;
213 if w <= width || space.is_none() {
214 let start = if let Some(sp) = space {
215 sp
216 } else {
217 if c.is_none() && text_pos == bol {
218 return out;
219 }
220 for _ in 0..indent.max(0) {
221 out.push(' ');
222 }
223 bol
224 };
225 out.push_str(&text[start..text_pos]);
226 if c.is_none() {
227 return out;
228 }
229 let cc = c.unwrap();
230 let mut new_space = text_pos;
231 if cc == b'\t' {
232 w |= 0x07;
233 } else if cc == b'\n' {
234 new_space += 1;
235 match bytes.get(new_space) {
236 Some(b'\n') => {
237 out.push('\n');
238 space = Some(new_space);
239 do_new_line = true;
240 }
241 nxt if !nxt.map(u8::is_ascii_alphanumeric).unwrap_or(false) => {
242 space = Some(new_space);
243 do_new_line = true;
244 }
245 _ => {
246 out.push(' ');
247 }
248 }
249 }
250 if !do_new_line {
251 space = Some(new_space);
252 w += 1;
253 text_pos += 1;
254 }
255 } else {
256 do_new_line = true;
257 }
258 if do_new_line {
259 out.push('\n');
260 let sp = space.unwrap();
261 text_pos = sp + usize::from(is_space_byte(bytes.get(sp).copied()));
262 bol = text_pos;
263 space = None;
264 w = indent2;
265 indent = indent2;
266 }
267 continue;
268 }
269 let ch = text[text_pos..].chars().next().unwrap();
271 let gw = UnicodeWidthChar::width(ch).unwrap_or(0) as i64;
272 w += gw;
273 text_pos += ch.len_utf8();
274 }
275}
276
277fn is_space_byte(b: Option<u8>) -> bool {
278 matches!(
279 b,
280 Some(b' ') | Some(b'\t') | Some(b'\n') | Some(b'\r') | Some(11) | Some(12)
281 )
282}
283
284#[must_use]
287pub fn add_indented_text(text: &str, indent1: i64, indent2: i64) -> String {
288 let indent1 = indent1.max(0);
289 let mut out = String::new();
290 let mut indent = indent1;
291 let bytes = text.as_bytes();
292 let mut pos = 0;
293 while pos < bytes.len() {
294 let eol = match bytes[pos..].iter().position(|&b| b == b'\n') {
295 Some(i) => pos + i + 1,
296 None => bytes.len(),
297 };
298 for _ in 0..indent {
299 out.push(' ');
300 }
301 out.push_str(&text[pos..eol]);
302 pos = eol;
303 indent = indent2;
304 }
305 out
306}
307
308#[cfg(test)]
309mod wrap_tests {
310 use super::add_wrapped_text;
311
312 #[test]
313 fn wrap_width_one_decoration_with_leading_newline() {
314 let input = "\n (tag: describe-me)%+w(2)";
317 assert_eq!(
318 add_wrapped_text(input, 0, 0, 1),
319 "\n(tag:\ndescribe-me)%+w(2)"
320 );
321 }
322
323 #[test]
324 fn wrap_zero_width_is_indent_only() {
325 assert_eq!(add_wrapped_text("a\nb", 2, 1, 0), " a\n b");
326 }
327
328 #[test]
329 fn wrap_simple_words() {
330 assert_eq!(add_wrapped_text("foo bar", 0, 0, 80), "foo bar");
332 }
333}