1use chrono::{FixedOffset, Local, TimeZone};
4use git2::{Commit, Time};
5use lazy_static::lazy_static;
6use std::fmt::Write;
7use std::str::FromStr;
8use textwrap::Options;
9use yansi::Paint;
10
11#[derive(Ord, PartialOrd, Eq, PartialEq)]
13pub enum CommitFormat {
14 OneLine,
15 Short,
16 Medium,
17 Full,
18 Format(String),
19}
20
21impl FromStr for CommitFormat {
22 type Err = String;
23
24 fn from_str(str: &str) -> Result<Self, Self::Err> {
25 match str {
26 "oneline" | "o" => Ok(CommitFormat::OneLine),
27 "short" | "s" => Ok(CommitFormat::Short),
28 "medium" | "m" => Ok(CommitFormat::Medium),
29 "full" | "f" => Ok(CommitFormat::Full),
30 str => Ok(CommitFormat::Format(str.to_string())),
31 }
32 }
33}
34
35const NEW_LINE: usize = 0;
36const HASH: usize = 1;
37const HASH_ABBREV: usize = 2;
38const PARENT_HASHES: usize = 3;
39const PARENT_HASHES_ABBREV: usize = 4;
40const REFS: usize = 5;
41const SUBJECT: usize = 6;
42const AUTHOR: usize = 7;
43const AUTHOR_EMAIL: usize = 8;
44const AUTHOR_DATE: usize = 9;
45const AUTHOR_DATE_SHORT: usize = 10;
46const AUTHOR_DATE_RELATIVE: usize = 11;
47const COMMITTER: usize = 12;
48const COMMITTER_EMAIL: usize = 13;
49const COMMITTER_DATE: usize = 14;
50const COMMITTER_DATE_SHORT: usize = 15;
51const COMMITTER_DATE_RELATIVE: usize = 16;
52const BODY: usize = 17;
53const BODY_RAW: usize = 18;
54
55const MODE_SPACE: usize = 1;
56const MODE_PLUS: usize = 2;
57const MODE_MINUS: usize = 3;
58
59lazy_static! {
60 pub static ref PLACEHOLDERS: Vec<[String; 4]> = {
61 let base = vec![
62 "n", "H", "h", "P", "p", "d", "s", "an", "ae", "ad", "as", "ar", "cn", "ce", "cd",
63 "cs", "cr", "b", "B",
64 ];
65 base.iter()
66 .map(|b| {
67 [
68 format!("%{}", b),
69 format!("% {}", b),
70 format!("%+{}", b),
71 format!("%-{}", b),
72 ]
73 })
74 .collect()
75 };
76}
77
78pub fn format_commit(
80 format: &str,
81 commit: &Commit,
82 branches: String,
83 wrapping: &Option<Options>,
84 hash_color: Option<u8>,
85) -> Result<Vec<String>, String> {
86 let mut replacements = vec![];
87
88 for (idx, arr) in PLACEHOLDERS.iter().enumerate() {
89 let mut curr = 0;
90 loop {
91 let mut found = false;
92 for (mode, str) in arr.iter().enumerate() {
93 if let Some(start) = &format[curr..format.len()].find(str) {
94 replacements.push((curr + start, str.len(), idx, mode));
95 curr += start + str.len();
96 found = true;
97 break;
98 }
99 }
100 if !found {
101 break;
102 }
103 }
104 }
105
106 replacements.sort_by_key(|p| p.0);
107
108 let mut lines = vec![];
109 let mut out = String::new();
110 if replacements.is_empty() {
111 write!(out, "{}", format).unwrap();
112 add_line(&mut lines, &mut out, wrapping);
113 } else {
114 let mut curr = 0;
115 for (start, len, idx, mode) in replacements {
116 if idx == NEW_LINE {
117 write!(out, "{}", &format[curr..start]).unwrap();
118 add_line(&mut lines, &mut out, wrapping);
119 } else {
120 write!(out, "{}", &format[curr..start]).unwrap();
121 let id = commit.id();
122 match idx {
123 HASH => {
124 match mode {
125 MODE_SPACE => write!(out, " ").unwrap(),
126 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
127 _ => {}
128 }
129 if let Some(color) = hash_color {
130 write!(out, "{}", id.to_string().fixed(color))
131 } else {
132 write!(out, "{}", id)
133 }
134 }
135 HASH_ABBREV => {
136 match mode {
137 MODE_SPACE => write!(out, " ").unwrap(),
138 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
139 _ => {}
140 }
141 if let Some(color) = hash_color {
142 write!(out, "{}", id.to_string()[..7].fixed(color))
143 } else {
144 write!(out, "{}", &id.to_string()[..7])
145 }
146 }
147 PARENT_HASHES => {
148 match mode {
149 MODE_SPACE => write!(out, " ").unwrap(),
150 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
151 _ => {}
152 }
153 for i in 0..commit.parent_count() {
154 write!(out, "{}", commit.parent_id(i).unwrap()).unwrap();
155 if i < commit.parent_count() - 1 {
156 write!(out, " ").unwrap();
157 }
158 }
159 Ok(())
160 }
161 PARENT_HASHES_ABBREV => {
162 match mode {
163 MODE_SPACE => write!(out, " ").unwrap(),
164 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
165 _ => {}
166 }
167 for i in 0..commit.parent_count() {
168 write!(
169 out,
170 "{}",
171 &commit
172 .parent_id(i)
173 .map_err(|err| err.to_string())?
174 .to_string()[..7]
175 )
176 .unwrap();
177 if i < commit.parent_count() - 1 {
178 write!(out, " ").unwrap();
179 }
180 }
181 Ok(())
182 }
183 REFS => {
184 match mode {
185 MODE_SPACE => {
186 if !branches.is_empty() {
187 write!(out, " ").unwrap()
188 }
189 }
190 MODE_PLUS => {
191 if !branches.is_empty() {
192 add_line(&mut lines, &mut out, wrapping)
193 }
194 }
195 MODE_MINUS => {
196 if branches.is_empty() {
197 out = remove_empty_lines(&mut lines, out)
198 }
199 }
200 _ => {}
201 }
202 write!(out, "{}", branches)
203 }
204 SUBJECT => {
205 let summary = commit.summary().unwrap_or("");
206 match mode {
207 MODE_SPACE => {
208 if !summary.is_empty() {
209 write!(out, " ").unwrap()
210 }
211 }
212 MODE_PLUS => {
213 if !summary.is_empty() {
214 add_line(&mut lines, &mut out, wrapping)
215 }
216 }
217 MODE_MINUS => {
218 if summary.is_empty() {
219 out = remove_empty_lines(&mut lines, out)
220 }
221 }
222 _ => {}
223 }
224 write!(out, "{}", summary)
225 }
226 AUTHOR => {
227 match mode {
228 MODE_SPACE => write!(out, " ").unwrap(),
229 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
230 _ => {}
231 }
232 write!(out, "{}", &commit.author().name().unwrap_or(""))
233 }
234 AUTHOR_EMAIL => {
235 match mode {
236 MODE_SPACE => write!(out, " ").unwrap(),
237 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
238 _ => {}
239 }
240 write!(out, "{}", &commit.author().email().unwrap_or(""))
241 }
242 AUTHOR_DATE => {
243 match mode {
244 MODE_SPACE => write!(out, " ").unwrap(),
245 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
246 _ => {}
247 }
248 write!(
249 out,
250 "{}",
251 format_date(commit.author().when(), "%a %b %e %H:%M:%S %Y %z")
252 )
253 }
254 AUTHOR_DATE_SHORT => {
255 match mode {
256 MODE_SPACE => write!(out, " ").unwrap(),
257 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
258 _ => {}
259 }
260 write!(out, "{}", format_date(commit.author().when(), "%F"))
261 }
262 AUTHOR_DATE_RELATIVE => {
263 match mode {
264 MODE_SPACE => write!(out, " ").unwrap(),
265 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
266 _ => {}
267 }
268 write!(out, "{}", format_relative_time(commit.author().when()))
269 }
270 COMMITTER => {
271 match mode {
272 MODE_SPACE => write!(out, " ").unwrap(),
273 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
274 _ => {}
275 }
276 write!(out, "{}", &commit.committer().name().unwrap_or(""))
277 }
278 COMMITTER_EMAIL => {
279 match mode {
280 MODE_SPACE => write!(out, " ").unwrap(),
281 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
282 _ => {}
283 }
284 write!(out, "{}", &commit.committer().email().unwrap_or(""))
285 }
286 COMMITTER_DATE => {
287 match mode {
288 MODE_SPACE => write!(out, " ").unwrap(),
289 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
290 _ => {}
291 }
292 write!(
293 out,
294 "{}",
295 format_date(commit.committer().when(), "%a %b %e %H:%M:%S %Y %z")
296 )
297 }
298 COMMITTER_DATE_SHORT => {
299 match mode {
300 MODE_SPACE => write!(out, " ").unwrap(),
301 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
302 _ => {}
303 }
304 write!(out, "{}", format_date(commit.committer().when(), "%F"))
305 }
306 COMMITTER_DATE_RELATIVE => {
307 match mode {
308 MODE_SPACE => write!(out, " ").unwrap(),
309 MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
310 _ => {}
311 }
312 write!(out, "{}", format_relative_time(commit.committer().when()))
313 }
314 BODY => {
315 let message = commit
316 .message()
317 .unwrap_or("")
318 .lines()
319 .collect::<Vec<&str>>();
320
321 let num_parts = message.len();
322 match mode {
323 MODE_SPACE => {
324 if num_parts > 2 {
325 write!(out, " ").unwrap()
326 }
327 }
328 MODE_PLUS => {
329 if num_parts > 2 {
330 add_line(&mut lines, &mut out, wrapping)
331 }
332 }
333 MODE_MINUS => {
334 if num_parts <= 2 {
335 out = remove_empty_lines(&mut lines, out)
336 }
337 }
338 _ => {}
339 }
340 for (cnt, line) in message.iter().enumerate() {
341 if cnt > 1 && (cnt < num_parts - 1 || !line.is_empty()) {
342 write!(out, "{}", line).unwrap();
343 add_line(&mut lines, &mut out, wrapping);
344 }
345 }
346 Ok(())
347 }
348 BODY_RAW => {
349 let message = commit
350 .message()
351 .unwrap_or("")
352 .lines()
353 .collect::<Vec<&str>>();
354
355 let num_parts = message.len();
356
357 match mode {
358 MODE_SPACE => {
359 if !message.is_empty() {
360 write!(out, " ").unwrap()
361 }
362 }
363 MODE_PLUS => {
364 if !message.is_empty() {
365 add_line(&mut lines, &mut out, wrapping)
366 }
367 }
368 MODE_MINUS => {
369 if message.is_empty() {
370 out = remove_empty_lines(&mut lines, out)
371 }
372 }
373 _ => {}
374 }
375 for (cnt, line) in message.iter().enumerate() {
376 if cnt < num_parts - 1 || !line.is_empty() {
377 write!(out, "{}", line).unwrap();
378 add_line(&mut lines, &mut out, wrapping);
379 }
380 }
381 Ok(())
382 }
383 x => return Err(format!("No commit field at index {}", x)),
384 }
385 .unwrap();
386 }
387 curr = start + len;
388 }
389 write!(out, "{}", &format[curr..(format.len())]).unwrap();
390 if !out.is_empty() {
391 add_line(&mut lines, &mut out, wrapping);
392 }
393 }
394 Ok(lines)
395}
396
397pub fn format_oneline(
399 commit: &Commit,
400 branches: String,
401 wrapping: &Option<Options>,
402 hash_color: Option<u8>,
403) -> Vec<String> {
404 let mut out = String::new();
405 let id = commit.id();
406 if let Some(color) = hash_color {
407 write!(out, "{}", id.to_string()[..7].fixed(color))
408 } else {
409 write!(out, "{}", &id.to_string()[..7])
410 }
411 .unwrap();
412
413 write!(out, "{} {}", branches, commit.summary().unwrap_or("")).unwrap();
414
415 if let Some(wrap) = wrapping {
416 textwrap::fill(&out, wrap)
417 .lines()
418 .map(|str| str.to_string())
419 .collect()
420 } else {
421 vec![out]
422 }
423}
424
425pub fn format(
427 commit: &Commit,
428 branches: String,
429 wrapping: &Option<Options>,
430 hash_color: Option<u8>,
431 format: &CommitFormat,
432) -> Result<Vec<String>, String> {
433 match format {
434 CommitFormat::OneLine => return Ok(format_oneline(commit, branches, wrapping, hash_color)),
435 CommitFormat::Format(format) => {
436 return format_commit(format, commit, branches, wrapping, hash_color)
437 }
438 _ => {}
439 }
440
441 let mut out_vec = vec![];
442 let mut out = String::new();
443
444 let id = commit.id();
445 if let Some(color) = hash_color {
446 write!(out, "commit {}", id.to_string().fixed(color))
447 } else {
448 write!(out, "commit {}", &id)
449 }
450 .map_err(|err| err.to_string())?;
451
452 write!(out, "{}", branches).map_err(|err| err.to_string())?;
453 append_wrapped(&mut out_vec, out, wrapping);
454
455 if commit.parent_count() > 1 {
456 out = String::new();
457 write!(
458 out,
459 "Merge: {} {}",
460 &commit.parent_id(0).unwrap().to_string()[..7],
461 &commit.parent_id(1).unwrap().to_string()[..7]
462 )
463 .map_err(|err| err.to_string())?;
464 append_wrapped(&mut out_vec, out, wrapping);
465 }
466
467 out = String::new();
468 write!(
469 out,
470 "Author: {} <{}>",
471 commit.author().name().unwrap_or(""),
472 commit.author().email().unwrap_or("")
473 )
474 .map_err(|err| err.to_string())?;
475 append_wrapped(&mut out_vec, out, wrapping);
476
477 if format > &CommitFormat::Medium {
478 out = String::new();
479 write!(
480 out,
481 "Commit: {} <{}>",
482 commit.committer().name().unwrap_or(""),
483 commit.committer().email().unwrap_or("")
484 )
485 .map_err(|err| err.to_string())?;
486 append_wrapped(&mut out_vec, out, wrapping);
487 }
488
489 if format > &CommitFormat::Short {
490 out = String::new();
491 write!(
492 out,
493 "Date: {}",
494 format_date(commit.author().when(), "%a %b %e %H:%M:%S %Y %z")
495 )
496 .map_err(|err| err.to_string())?;
497 append_wrapped(&mut out_vec, out, wrapping);
498 }
499
500 if format == &CommitFormat::Short {
501 out_vec.push("".to_string());
502 append_wrapped(
503 &mut out_vec,
504 format!(" {}", commit.summary().unwrap_or("")),
505 wrapping,
506 );
507 out_vec.push("".to_string());
508 } else {
509 out_vec.push("".to_string());
510 let mut add_line = true;
511 for line in commit.message().unwrap_or("").lines() {
512 if line.is_empty() {
513 out_vec.push(line.to_string());
514 } else {
515 append_wrapped(&mut out_vec, format!(" {}", line), wrapping);
516 }
517 add_line = !line.trim().is_empty();
518 }
519 if add_line {
520 out_vec.push("".to_string());
521 }
522 }
523
524 Ok(out_vec)
525}
526
527pub fn format_date(time: Time, format: &str) -> String {
528 let offset = FixedOffset::east_opt(time.offset_minutes() * 60).expect("Invalid offset minutes");
529 let date = offset
530 .timestamp_opt(time.seconds(), 0)
531 .single()
532 .expect("Invalid timestamp, maybe a fold or gap in local time");
533 date.format(format).to_string()
534}
535
536pub fn format_relative_time(time: Time) -> String {
538 let offset = FixedOffset::east_opt(time.offset_minutes() * 60).expect("Invalid offset minutes");
539 let commit_time = Local::from_offset(&offset)
540 .timestamp_opt(time.seconds(), 0)
541 .single()
542 .expect("Invalid timestamp");
543 let now = Local::now();
544 let duration = now.signed_duration_since(commit_time);
545
546 let seconds = duration.num_seconds();
547 let minutes = duration.num_minutes();
548 let hours = duration.num_hours();
549 let days = duration.num_hours() / 24;
550 let weeks = days / 7;
551 let months = days / 30;
552 let years = days / 365;
553
554 if seconds < 60 {
555 format!("{} seconds ago", seconds)
556 } else if minutes < 60 {
557 format!("{} minutes ago", minutes)
558 } else if hours < 24 {
559 format!("{} hours ago", hours)
560 } else if days < 7 {
561 format!("{} days ago", days)
562 } else if weeks < 4 {
563 format!("{} weeks ago", weeks)
564 } else if months < 12 {
565 format!("{} months ago", months)
566 } else {
567 format!("{} years ago", years)
568 }
569}
570
571fn append_wrapped(vec: &mut Vec<String>, str: String, wrapping: &Option<Options>) {
572 if str.is_empty() {
573 vec.push(str);
574 } else if let Some(wrap) = wrapping {
575 vec.extend(
576 textwrap::fill(&str, wrap)
577 .lines()
578 .map(|str| str.to_string()),
579 )
580 } else {
581 vec.push(str);
582 }
583}
584
585fn add_line(lines: &mut Vec<String>, line: &mut String, wrapping: &Option<Options>) {
586 let mut temp = String::new();
587 std::mem::swap(&mut temp, line);
588 append_wrapped(lines, temp, wrapping);
589}
590
591fn remove_empty_lines(lines: &mut Vec<String>, mut line: String) -> String {
592 while !lines.is_empty() && lines.last().unwrap().is_empty() {
593 line = lines.remove(lines.len() - 1);
594 }
595 if !lines.is_empty() {
596 line = lines.remove(lines.len() - 1);
597 }
598 line
599}