git_graph/print/
format.rs

1//! Formatting of commits.
2
3use 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/// Commit formatting options.
12#[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
76/// Format a commit for `CommitFormat::Format(String)`.
77pub 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
382/// Format a commit for `CommitFormat::OneLine`.
383pub 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
413/// Format a commit for `CommitFormat::Short`, `CommitFormat::Medium` or `CommitFormat::Full`.
414pub 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}