summaryrefslogtreecommitdiffstats
path: root/src/handlers/blame.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/handlers/blame.rs')
-rw-r--r--src/handlers/blame.rs574
1 files changed, 0 insertions, 574 deletions
diff --git a/src/handlers/blame.rs b/src/handlers/blame.rs
deleted file mode 100644
index 7578a5c4..00000000
--- a/src/handlers/blame.rs
+++ /dev/null
@@ -1,574 +0,0 @@
-use chrono::{DateTime, FixedOffset};
-use lazy_static::lazy_static;
-use regex::Regex;
-use std::borrow::Cow;
-use unicode_width::UnicodeWidthStr;
-
-use crate::ansi::measure_text_width;
-use crate::color;
-use crate::config;
-use crate::config::delta_unreachable;
-use crate::delta::{self, State, StateMachine};
-use crate::fatal;
-use crate::format::{self, FormatStringSimple, Placeholder};
-use crate::format::{make_placeholder_regex, parse_line_number_format};
-use crate::paint::{self, BgShouldFill, StyleSectionSpecifier};
-use crate::style::Style;
-use crate::utils;
-
-#[derive(Clone, Debug)]
-pub enum BlameLineNumbers {
- // "none" equals a fixed string with just a separator
- On(FormatStringSimple),
- PerBlock(FormatStringSimple),
- Every(usize, FormatStringSimple),
-}
-
-impl<'a> StateMachine<'a> {
- /// If this is a line of git blame output then render it accordingly. If
- /// this is the first blame line, then set the syntax-highlighter language
- /// according to delta.default-language.
- pub fn handle_blame_line(&mut self) -> std::io::Result<bool> {
- // TODO: It should be possible to eliminate some of the .clone()s and
- // .to_owned()s.
- let mut handled_line = false;
- self.painter.emit()?;
- let (previous_key, try_parse) = match &self.state {
- State::Blame(key) => (Some(key.clone()), true),
- State::Unknown => (None, true),
- _ => (None, false),
- };
- if try_parse {
- let line = self.line.to_owned();
- if let Some(blame) = parse_git_blame_line(&line, &self.config.blame_timestamp_format) {
- // Format blame metadata
- let format_data = format::parse_line_number_format(
- &self.config.blame_format,
- &BLAME_PLACEHOLDER_REGEX,
- false,
- );
- let mut formatted_blame_metadata =
- format_blame_metadata(&format_data, &blame, self.config);
- let key = formatted_blame_metadata.clone();
- let is_repeat = previous_key.as_deref() == Some(&key);
- if is_repeat {
- formatted_blame_metadata =
- " ".repeat(measure_text_width(&formatted_blame_metadata))
- };
- let metadata_style =
- self.blame_metadata_style(&key, previous_key.as_deref(), is_repeat);
- let code_style = self.config.blame_code_style.unwrap_or(metadata_style);
- let separator_style = self.config.blame_separator_style.unwrap_or(code_style);
-
- let (nr_prefix, line_number, nr_suffix) = format_blame_line_number(
- &self.config.blame_separator_format,
- blame.line_number,
- is_repeat,
- );
-
- write!(
- self.painter.writer,
- "{}{}{}{}",
- metadata_style.paint(&formatted_blame_metadata),
- separator_style.paint(nr_prefix),
- metadata_style.paint(&line_number),
- separator_style.paint(nr_suffix),
- )?;
-
- // Emit syntax-highlighted code
- if matches!(self.state, State::Unknown) {
- if let Some(lang) = utils::process::git_blame_filename_extension()
- .as_ref()
- .or(self.config.default_language.as_ref())
- {
- self.painter.set_syntax(Some(lang));
- self.painter.set_highlighter();
- }
- }
- self.state = State::Blame(key);
- self.painter.syntax_highlight_and_paint_line(
- &format!("{}\n", blame.code),
- StyleSectionSpecifier::Style(code_style),
- self.state.clone(),
- BgShouldFill::default(),
- );
- handled_line = true
- }
- }
- Ok(handled_line)
- }
-
- fn blame_metadata_style(
- &mut self,
- key: &str,
- previous_key: Option<&str>,
- is_repeat: bool,
- ) -> Style {
- let mut style = match paint::parse_style_sections(&self.raw_line, self.config).first() {
- Some((style, _)) if style != &Style::default() => {
- // Something like `blame.coloring = highlightRecent` is in effect; honor
- // the color from git, subject to map-styles.
- *style
- }
- _ => {
- // Compute the color ourselves.
- let color = self.get_color(key, previous_key, is_repeat);
- // TODO: This will often be pointlessly updating a key with the
- // value it already has. It might be nicer to do this (and
- // compute the style) in get_color(), but as things stand the
- // borrow checker won't permit that.
- let style = Style::from_colors(
- None,
- color::parse_color(&color, true, self.config.git_config()),
- );
- self.blame_key_colors.insert(key.to_owned(), color);
- style
- }
- };
-
- style.is_syntax_highlighted = true;
- style
- }
-
- fn get_color(&self, this_key: &str, previous_key: Option<&str>, is_repeat: bool) -> String {
- // Determine color for this line
- let previous_key_color = match previous_key {
- Some(previous_key) => self.blame_key_colors.get(previous_key),
- None => None,
- };
-
- match (
- self.blame_key_colors.get(this_key),
- previous_key_color,
- is_repeat,
- ) {
- (Some(key_color), Some(previous_key_color), true) => {
- debug_assert!(key_color == previous_key_color);
- // Repeated key: assign same color
- key_color.to_owned()
- }
- (None, Some(previous_key_color), false) => {
- // The key has no color: assign the next color that differs
- // from previous key.
- self.get_next_color(Some(previous_key_color))
- }
- (None, None, false) => {
- // The key has no color, and there is no previous key:
- // Just assign the next color. is_repeat is necessarily false.
- self.get_next_color(None)
- }
- (Some(key_color), Some(previous_key_color), false) => {
- if key_color != previous_key_color {
- // Consecutive keys differ without a collision
- key_color.to_owned()
- } else {
- // Consecutive keys differ; prevent color collision
- self.get_next_color(Some(key_color))
- }
- }
- (None, _, true) => delta_unreachable("is_repeat cannot be true when key has no color."),
- (Some(_), None, _) => {
- delta_unreachable("There must be a previous key if the key has a color.")
- }
- }
- }
-
- fn get_next_color(&self, other_than_color: Option<&str>) -> String {
- let n_keys = self.blame_key_colors.len();
- let n_colors = self.config.blame_palette.len();
- let color = self.config.blame_palette[n_keys % n_colors].clone();
- if Some(color.as_str()) != other_than_color {
- color
- } else {
- self.config.blame_palette[(n_keys + 1) % n_colors].clone()
- }
- }
-}
-
-#[derive(Debug)]
-pub struct BlameLine<'a> {
- pub commit: &'a str,
- pub author: &'a str,
- pub time: DateTime<FixedOffset>,
- pub line_number: usize,
- pub code: &'a str,
-}
-
-// E.g.
-//ea82f2d0 (Dan Davison 2021-08-22 18:20:19 -0700 120) let mut handled_line = self.handle_key_meta_header_line()?
-
-lazy_static! {
- static ref BLAME_LINE_REGEX: Regex = Regex::new(
- r"(?x)
-^
-(
- \^?[0-9a-f]{4,40} # commit hash (^ is 'boundary commit' marker)
-)
-(?: [^(]+)? # optional file name (unused; present if file has been renamed; TODO: inefficient?)
-[\ ]
-\( # open ( which the previous file name may not contain in case a name does (which is more likely)
-(
- [^\ ].*[^\ ] # author name
-)
-[\ ]+
-( # timestamp
- [0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}\ [-+][0-9]{4}
-)
-[\ ]+
-(
- [0-9]+ # line number
-)
-\) # close )
-(
- .* # code, with leading space
-)
-$
-"
- )
- .unwrap();
-}
-
-pub fn parse_git_blame_line<'a>(line: &'a str, timestamp_format: &str) -> Option<BlameLine<'a>> {
- let caps = BLAME_LINE_REGEX.captures(line)?;
-
- let commit = caps.get(1).unwrap().as_str();
- let author = caps.get(2).unwrap().as_str();
- let timestamp = caps.get(3).unwrap().as_str();
-
- let time = DateTime::parse_from_str(timestamp, timestamp_format).ok()?;
-
- let line_number = caps.get(4).unwrap().as_str().parse::<usize>().ok()?;
-
- let code = caps.get(5).unwrap().as_str();
-
- Some(BlameLine {
- commit,
- author,
- time,
- line_number,
- code,
- })
-}
-
-lazy_static! {
- // line numbers (`{n}`) change with every line and are set separately via `blame-separator-format`
- pub static ref BLAME_PLACEHOLDER_REGEX: Regex =
- format::make_placeholder_regex(&["timestamp", "author", "commit"]);
-}
-
-pub fn format_blame_metadata(
- format_data: &[format::FormatStringPlaceholderData],
- blame: &BlameLine,
- config: &config::Config,
-) -> String {
- let mut s = String::new();
- let mut suffix = "";
- for placeholder in format_data {
- s.push_str(placeholder.prefix.as_str());
-
- let alignment_spec = placeholder.alignment_spec.unwrap_or(format::Align::Left);
- let width = placeholder.width.unwrap_or(15);
-
- let field = match placeholder.placeholder {
- Some(Placeholder::Str("timestamp")) => {
- Some(Cow::from(match &config.blame_timestamp_output_format {
- Some(time_format) => blame.time.format(time_format).to_string(),
- None => chrono_humanize::HumanTime::from(blame.time).to_string(),
- }))
- }
- Some(Placeholder::Str("author")) => Some(Cow::from(blame.author)),
- Some(Placeholder::Str("commit")) => Some(delta::format_raw_line(blame.commit, config)),
- None => None,
- _ => unreachable!("Unexpected `git blame` input"),
- };
- if let Some(field) = field {
- // Unicode modifier should not be counted as character to allow a consistent padding
- let unicode_modifier_width =
- field.as_ref().chars().count() - UnicodeWidthStr::width(field.as_ref());
- s.push_str(&format::pad(
- &field,
- width + unicode_modifier_width,
- alignment_spec,
- placeholder.precision,
- ))
- }
- suffix = placeholder.suffix.as_str();
- }
- s.push_str(suffix);
- s
-}
-
-pub fn format_blame_line_number(
- format: &BlameLineNumbers,
- line_number: usize,
- is_repeat: bool,
-) -> (&str, String, &str) {
- let (format, empty) = match &format {
- BlameLineNumbers::PerBlock(format) => (format, is_repeat),
- BlameLineNumbers::Every(n, format) => (format, is_repeat && line_number % n != 0),
- BlameLineNumbers::On(format) => (format, false),
- };
- let mut result = String::new();
-
- // depends on defaults being set when parsing arguments
- let line_number = if format.width.is_some() {
- format::pad(
- line_number,
- format.width.unwrap(),
- format.alignment_spec.unwrap(),
- None,
- )
- } else {
- String::new()
- };
-
- if empty {
- for _ in 0..measure_text_width(&line_number) {
- result.push(' ');
- }
- } else {
- result.push_str(&line_number);
- }
-
- (format.prefix.as_str(), result, format.suffix.as_str())
-}
-
-pub fn parse_blame_line_numbers(arg: &str) -> BlameLineNumbers {
- if arg == "none" {
- return BlameLineNumbers::On(crate::format::FormatStringSimple::only_string("│"));
- }
-
- let regex = make_placeholder_regex(&["n"]);
- let f = match parse_line_number_format(arg, &regex, false) {
- v if v.len() > 1 => {
- fatal("Too many format arguments numbers for blame-line-numbers".to_string())
- }
- mut v => v.pop().unwrap(),
- };
-
- let set_defaults = |mut format: crate::format::FormatStringSimple| {
- format.width = format.width.or(Some(4));
- format.alignment_spec = format.alignment_spec.or(Some(crate::format::Align::Center));
-
- format
- };
-
- if f.placeholder.is_none() {
- return BlameLineNumbers::On(crate::format::FormatStringSimple::only_string(
- f.suffix.as_str(),
- ));
- }
-
- match f.fmt_type.as_str() {
- t if t.is_empty() || t == "every" => BlameLineNumbers::On(set_defaults(f.into_simple())),
- t if t == "block" => BlameLineNumbers::PerBlock(set_defaults(f.into_simple())),
- every_n if every_n.starts_with("every-") => {
- let n = every_n["every-".len()..]
- .parse::<usize>()
- .unwrap_or_else(|err| {
- fatal(format!(
- "Invalid number for blame-line-numbers in every-N argument: {err}",
- ))
- });
-
- if n > 1 {
- BlameLineNumbers::Every(n, set_defaults(f.into_simple()))
- } else {
- BlameLineNumbers::On(set_defaults(f.into_simple()))
- }
- }
- t => fatal(format!(
- "Invalid format type \"{t}\" for blame-line-numbers",
- )),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use itertools::Itertools;
- use std::{collections::HashMap, io::Cursor};
-
- use crate::tests::integration_test_utils;
-
- use super::*;
-
- #[test]
- fn test_blame_line_regex() {
- for line in &[
- "ea82f2d0 (Dan Davison 2021-08-22 18:20:19 -0700 120) let mut handled_line = self.handle_commit_meta_header_line()?",
- "b2257cfa (Dan Davison 2020-07-18 15:34:43 -0400 1) use std::borrow::Cow;",
- "^35876eaa (Nicholas Marriott 2009-06-01 22:58:49 +0000 38) /* Default grid cell data. */",
- ] {
- let caps = BLAME_LINE_REGEX.captures(line);
- assert!(caps.is_some());
- assert!(parse_git_blame_line(line, "%Y-%m-%d %H:%M:%S %z").is_some());
- }
- }
-
- #[test]
- fn test_blame_line_with_parens_in_name() {
- let line =
- "61f180c8 (Kangwook Lee (이강욱) 2021-06-09 23:33:59 +0900 130) let mut output_type =";
- let caps = BLAME_LINE_REGEX.captures(line).unwrap();
- assert_eq!(caps.get(2).unwrap().as_str(), "Kangwook Lee (이강욱)");
- }
-
- #[test]
- fn test_format_blame_metadata_with_default_timestamp_output_format() {
- let format_data = make_format_data_with_placeholder("timestamp");
- let blame = make_blame_line_with_time("1996-12-19T16:39:57-08:00");
- let config = integration_test_utils::make_config_from_args(&[]);
- let regex = Regex::new(r"^\d+ years ago$").unwrap();
- let result = format_blame_metadata(&[format_data], &blame, &config);
- assert!(regex.is_match(result.trim()));
- }
-
- #[test]
- fn test_format_blame_metadata_with_custom_timestamp_output_format() {
- let format_data = make_format_data_with_placeholder("timestamp");
- let blame = make_blame_line_with_time("1996-12-19T16:39:57-08:00");
- let config = integration_test_utils::make_config_from_args(&[
- "--blame-timestamp-output-format=%Y-%m-%d %H:%M",
- ]);
- let result = format_blame_metadata(&[format_data], &blame, &config);
- assert_eq!(result.trim(), "1996-12-19 16:39");
- }
-
- #[test]
- fn test_format_blame_metadata_with_accent_in_name() {
- let config = integration_test_utils::make_config_from_args(&[]);
-
- let count_trailing_spaces = |s: String| s.chars().rev().filter(|&c| c == ' ').count();
-
- let format_data1 = make_format_data_with_placeholder("author");
- let blame1 = make_blame_line_with_author("E\u{301}dith Piaf");
- let result1 = format_blame_metadata(&[format_data1], &blame1, &config);
-
- let format_data2 = make_format_data_with_placeholder("author");
- let blame2 = make_blame_line_with_author("Edith Piaf");
- let result2 = format_blame_metadata(&[format_data2], &blame2, &config);
-
- assert_eq!(
- count_trailing_spaces(result1),
- count_trailing_spaces(result2)
- );
- }
-
- #[test]
- fn test_color_assignment() {
- let mut writer = Cursor::new(vec![0; 512]);
- let config = integration_test_utils::make_config_from_args(&[
- "--blame-format",
- "{author} {commit}",
- "--blame-palette",
- "1 2",
- ]);
- let mut machine = StateMachine::new(&mut writer, &config);
-
- let blame_lines: HashMap<&str, &str> = vec![
- (
- "A",
- "aaaaaaa (Dan Davison 2021-08-22 18:20:19 -0700 120) A",
- ),
- (
- "B",
- "bbbbbbb (Dan Davison 2020-07-18 15:34:43 -0400 1) B",
- ),
- (
- "C",
- "ccccccc (Dan Davison 2020-07-18 15:34:43 -0400 1) C",
- ),
- ]
- .into_iter()
- .collect();
-
- // First key gets first color
- machine.line = blame_lines["A"].into();
- machine.handle_blame_line().unwrap();
- assert_eq!(
- hashmap_items(&machine.blame_key_colors),
- &[("Dan Davison aaaaaaa ", "1")]
- );
-
- // Repeat key: same color
- machine.line = blame_lines["A"].into();
- machine.handle_blame_line().unwrap();
- assert_eq!(
- hashmap_items(&machine.blame_key_colors),
- &[("Dan Davison aaaaaaa ", "1")]
- );
-
- // Second distinct key gets second color
- machine.line = blame_lines["B"].into();
- machine.handle_blame_line().unwrap();
- assert_eq!(
- hashmap_items(&machine.blame_key_colors),
- &[
- ("Dan Davison aaaaaaa ", "1"),
- ("Dan Davison bbbbbbb ", "2")
- ]
- );
-
- // Third distinct key gets first color (we only have 2 colors)
- machine.line = blame_lines["C"].into();
- machine.handle_blame_line().unwrap();
- assert_eq!(
- hashmap_items(&machine.blame_key_colors),
- &[
- ("Dan Davison aaaaaaa ", "1"),
- ("Dan Davison bbbbbbb ", "2"),
- ("Dan Davison ccccccc ", "1")
- ]
- );
-
- // Now the first key appears again. It would get the first color, but
- // that would be a consecutive-key-color-collision. So it gets the
- // second color.
- machine.line = blame_lines["A"].into();
- machine.handle_blame_line().unwrap();
- assert_eq!(
- hashmap_items(&machine.blame_key_colors),
- &[
- ("Dan Davison aaaaaaa ", "2"),
- ("Dan Davison bbbbbbb ", "2"),
- ("Dan Davison ccccccc ", "1")
- ]
- );
- }
-
- fn hashmap_items(hashmap: &HashMap<String, String>) -> Vec<(&str, &str)> {
- hashmap
- .iter()
- .sorted()
- .map(|(k, v)| (k.as_str(), v.as_str()))
- .collect()
- }
-
- fn make_blame_line_with_time(timestamp: &str) -> BlameLine {
- let time = chrono::DateTime::parse_from_rfc3339(timestamp).unwrap();
- BlameLine {
- commit: "",
- author: "",
- time,
- line_number: 0,
- code: "",
- }
- }
-
- fn make_format_data_with_placeholder(placeholder: &str) -> format::FormatStringPlaceholderData {
- format::FormatStringPlaceholderData {
- placeholder: Some(Placeholder::Str(placeholder)),
- ..Default::default()
- }
- }
-
- fn make_blame_line_with_author(author: &str) -> BlameLine {
- BlameLine {
- commit: "",
- author,
- time: chrono::DateTime::default(),
- line_number: 0,
- code: "",
- }
- }
-}