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