diff options
-rw-r--r-- | Cargo.lock | 25 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/delta.rs | 2 | ||||
-rw-r--r-- | src/handlers/blame.rs | 183 | ||||
-rw-r--r-- | src/handlers/mod.rs | 1 | ||||
-rw-r--r-- | src/paint.rs | 2 |
6 files changed, 215 insertions, 0 deletions
@@ -145,8 +145,20 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ + "libc", "num-integer", "num-traits", + "time", + "winapi", +] + +[[package]] +name = "chrono-humanize" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eddc119501d583fd930cb92144e605f44e0252c38dd89d9247fffa1993375cb" +dependencies = [ + "chrono", ] [[package]] @@ -320,6 +332,8 @@ dependencies = [ "bitflags", "box_drawing", "bytelines", + "chrono", + "chrono-humanize", "console", "ctrlc", "dirs-next", @@ -897,6 +911,17 @@ dependencies = [ ] [[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] name = "tinyvec" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -15,6 +15,8 @@ name = "delta" path = "src/main.rs" [dependencies] +chrono = "0.4.19" +chrono-humanize = "0.2.1" ansi_colours = "1.0.4" ansi_term = "0.12.1" atty = "0.2.14" diff --git a/src/delta.rs b/src/delta.rs index cab9d6fb..92d8691f 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -22,6 +22,7 @@ pub enum State { HunkPlus(Option<String>), // In hunk; added line (raw_line) SubmoduleLog, // In a submodule section, with gitconfig diff.submodule = log SubmoduleShort(String), // In a submodule section, with gitconfig diff.submodule = short + Blame(String), // In a line of `git blame` output. Unknown, // The following elements are created when a line is wrapped to display it: HunkZeroWrapped, // Wrapped unchanged line @@ -119,6 +120,7 @@ impl<'a> StateMachine<'a> { || self.handle_submodule_log_line()? || self.handle_submodule_short_line()? || self.handle_hunk_line()? + || self.handle_blame_line()? || self.should_skip_line() || self.emit_line_unchanged()?; } diff --git a/src/handlers/blame.rs b/src/handlers/blame.rs new file mode 100644 index 00000000..5cbeaa2c --- /dev/null +++ b/src/handlers/blame.rs @@ -0,0 +1,183 @@ +use chrono::{DateTime, FixedOffset}; +use lazy_static::lazy_static; +use regex::Regex; + +use crate::color; +use crate::config; +use crate::delta::{self, State, StateMachine}; +use crate::format::{self, Placeholder}; +use crate::paint::BgShouldFill; +use crate::style::Style; + +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> { + let mut handled_line = false; + self.painter.emit()?; + if matches!(self.state, State::Unknown | State::Blame(_)) { + if let Some(blame) = + parse_git_blame_line(&self.line, &self.config.blame_timestamp_format) + { + // Determine color for this line + let color = if let Some(color) = self.blame_commit_colors.get(blame.commit) { + color + } else { + let n_commits = self.blame_commit_colors.len(); + let n_colors = self.config.blame_palette.len(); + let new_color = &self.config.blame_palette[(n_commits + 1) % n_colors]; + self.blame_commit_colors + .insert(blame.commit.to_owned(), new_color.to_owned()); + new_color + }; + let mut style = Style::from_colors(None, color::parse_color(color, true)); + style.is_syntax_highlighted = true; + + // Construct commit metadata, paint, and emit + let format_data = format::parse_line_number_format( + &self.config.blame_format, + &*BLAME_PLACEHOLDER_REGEX, + false, + ); + write!( + self.painter.writer, + "{}", + style.paint(format_blame_metadata(&format_data, &blame, self.config)) + )?; + + // Emit syntax-highlighted code + if matches!(self.state, State::Unknown) { + if let Some(lang) = self.config.default_language.as_ref() { + self.painter.set_syntax(Some(lang)); + self.painter.set_highlighter(); + } + self.state = State::Blame(blame.commit.to_owned()); + } + self.painter.syntax_highlight_and_paint_line( + blame.code, + style, + self.state.clone(), + BgShouldFill::default(), + ); + handled_line = true + } + } + Ok(handled_line) + } +} + +#[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_commit_meta_header_line()? + +lazy_static! { + static ref BLAME_LINE_REGEX: Regex = Regex::new( + r"(?x) +^ +( + [0-9a-f]{8} # commit hash +) +[\ ] +\( # open ( +( + [^\ ].*[^\ ] # 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! { + 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 + .as_ref() + .unwrap_or(&format::Align::Left); + let width = placeholder.width.unwrap_or(15); + + let pad = |s| format::pad(s, width, alignment_spec); + match placeholder.placeholder { + Some(Placeholder::Str("timestamp")) => s.push_str(&pad( + &chrono_humanize::HumanTime::from(blame.time).to_string(), + )), + Some(Placeholder::Str("author")) => s.push_str(&pad(blame.author)), + Some(Placeholder::Str("commit")) => { + s.push_str(&pad(&delta::format_raw_line(blame.commit, config))) + } + None => {} + _ => unreachable!("Unexpected `git blame` input"), + } + suffix = placeholder.suffix.as_str(); + } + s.push_str(suffix); + s +} + +#[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;" + ] { + 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()); + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 6302c739..db27e28f 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,5 +1,6 @@ /// This module contains functions handling input lines encountered during the /// main `StateMachine::consume()` loop. +pub mod blame; pub mod commit_meta; pub mod diff_stat; pub mod draw; diff --git a/src/paint.rs b/src/paint.rs index bcf3e941..3dd4ae4a 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -468,6 +468,7 @@ impl<'a> Painter<'a> { (config.plus_style, config.plus_non_emph_style) } } + State::Blame(_) => (diff_sections[0].0, diff_sections[0].0), _ => (config.null_style, config.null_style), }; let fill_style = if style_sections_contain_more_than_one_style(diff_sections) { @@ -621,6 +622,7 @@ impl<'a> Painter<'a> { } State::HunkHeader(_, _) => true, State::HunkMinus(Some(_)) | State::HunkPlus(Some(_)) => false, + State::Blame(_) => true, _ => panic!( "should_compute_syntax_highlighting is undefined for state {:?}", state |