Skip to content

Commit

Permalink
Handle blame output
Browse files Browse the repository at this point in the history
Fixes #291, #426
  • Loading branch information
dandavison committed Aug 29, 2021
1 parent 6afd370 commit 19eec9b
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 9 deletions.
33 changes: 32 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
122 changes: 122 additions & 0 deletions src/blame.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use chrono::{DateTime, FixedOffset};
use lazy_static::lazy_static;
use regex::Regex;

use crate::config;
use crate::delta;
use crate::format;

#[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>> {
if let Some(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();
if let Ok(time) = DateTime::parse_from_str(timestamp, timestamp_format) {
let line_number_str = caps.get(4).unwrap().as_str();
if let Ok(line_number) = line_number_str.parse::<usize>() {
let code = caps.get(5).unwrap().as_str();
Some(BlameLine {
commit,
author,
time,
line_number,
code,
})
} else {
None
}
} else {
None
}
} else {
None
}
}

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);

let alignment_spec = placeholder.alignment_spec.unwrap_or("<");
let width = placeholder.width.unwrap_or(15);

let pad = |s| format::pad(s, width, alignment_spec);
match placeholder.placeholder {
Some("timestamp") => s.push_str(&pad(
&chrono_humanize::HumanTime::from(blame.time).to_string()
)),
Some("author") => s.push_str(&pad(blame.author)),
Some("commit") => s.push_str(&pad(&delta::format_raw_line(blame.commit, config))),
None => {}
Some(_) => unreachable!(),
}
suffix = placeholder.suffix;
}
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());
}
}
27 changes: 24 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,9 +419,30 @@ pub struct Opt {
/// (underline), 'ol' (overline), or the combination 'ul ol'.
pub hunk_header_decoration_style: String,

// Default language used for syntax highlighting when this cannot be
// inferred from a filename. It will typically make sense to set this in
// per-repository git config ().git/config)
/// Format string for git blame commit metadata. Available placeholders are
/// "{timestamp}", "{author}", and "{commit}".
#[structopt(
long = "blame-format",
default_value = "{timestamp:<15} {author:<15} {commit:<8} │ "
)]
pub blame_format: String,

/// Background colors used for git blame lines (space-separated string).
/// Lines added by the same commit are painted with the same color; colors
/// are recycled as needed.
#[structopt(long = "blame-palette")]
pub blame_palette: Option<String>,

/// Format of `git blame` timestamp in raw git output received by delta.
#[structopt(
long = "blame-timestamp-format",
default_value = "%Y-%m-%d %H:%M:%S %z"
)]
pub blame_timestamp_format: String,

/// Default language used for syntax highlighting when this cannot be
/// inferred from a filename. It will typically make sense to set this in
/// per-repository git config ().git/config)
#[structopt(long = "default-language")]
pub default_language: Option<String>,

Expand Down
10 changes: 10 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ use crate::style::{self, Style};
pub struct Config {
pub available_terminal_width: usize,
pub background_color_extends_to_terminal_width: bool,
pub blame_format: String,
pub blame_palette: Option<Vec<String>>,
pub blame_timestamp_format: String,
pub commit_style: Style,
pub color_only: bool,
pub commit_regex: Regex,
Expand Down Expand Up @@ -202,6 +205,13 @@ impl From<cli::Opt> for Config {
background_color_extends_to_terminal_width: opt
.computed
.background_color_extends_to_terminal_width,
blame_format: opt.blame_format,
blame_palette: opt.blame_palette.map(|s| {
s.split_whitespace()
.map(|s| s.to_owned())
.collect::<Vec<String>>()
}),
blame_timestamp_format: opt.blame_timestamp_format,
commit_style,
color_only: opt.color_only,
commit_regex,
Expand Down
77 changes: 74 additions & 3 deletions src/delta.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::io::BufRead;
use std::io::Write;

use bytelines::ByteLines;
use unicode_segmentation::UnicodeSegmentation;

use crate::ansi;
use crate::blame;
use crate::cli;
use crate::color;
use crate::config::Config;
use crate::draw;
use crate::features;
use crate::format;
use crate::hunk_header;
use crate::paint::Painter;
use crate::parse;
use crate::style::{self, DecorationStyle};
use crate::style::{self, DecorationStyle, Style};

#[derive(Clone, Debug, PartialEq)]
pub enum State {
Expand All @@ -26,6 +29,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,
}

Expand Down Expand Up @@ -77,6 +81,7 @@ struct StateMachine<'a> {
// avoid emitting the file meta header line twice (#245).
current_file_pair: Option<(String, String)>,
handled_file_meta_header_line_file_pair: Option<(String, String)>,
blame_commit_colors: HashMap<String, String>,
}

pub fn delta<I>(lines: ByteLines<I>, writer: &mut dyn Write, config: &Config) -> std::io::Result<()>
Expand All @@ -102,6 +107,7 @@ impl<'a> StateMachine<'a> {
handled_file_meta_header_line_file_pair: None,
painter: Painter::new(writer, config),
config,
blame_commit_colors: HashMap::new(),
}
}

Expand All @@ -126,7 +132,8 @@ impl<'a> StateMachine<'a> {
|| self.handle_additional_file_meta_cases()?
|| self.handle_submodule_log_line()?
|| self.handle_submodule_short_line()?
|| self.handle_hunk_line()?;
|| self.handle_hunk_line()?
|| self.handle_blame_line()?;

if self.state == State::FileMeta && self.should_handle() && !self.config.color_only {
// Skip file metadata lines unless a raw diff style has been requested.
Expand Down Expand Up @@ -463,6 +470,70 @@ impl<'a> StateMachine<'a> {
Ok(true)
}

/// 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.
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) =
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.as_ref().map(|v| v.len()).unwrap();
let new_color = self
.config
.blame_palette
.as_ref()
.map(|v| &v[(n_commits + 1) % n_colors])
.unwrap();
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::BLAME_PLACEHOLDER_REGEX,
);
write!(
self.painter.writer,
"{}",
style.paint(blame::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(),
true,
);
handled_line = true
}
}
Ok(handled_line)
}

fn _handle_additional_cases(&mut self, to_state: State) -> std::io::Result<bool> {
let mut handled_line = false;

Expand Down Expand Up @@ -635,7 +706,7 @@ impl<'a> StateMachine<'a> {

/// If output is going to a tty, emit hyperlinks if requested.
// Although raw output should basically be emitted unaltered, we do this.
fn format_raw_line<'a>(line: &'a str, config: &Config) -> Cow<'a, str> {
pub fn format_raw_line<'a>(line: &'a str, config: &Config) -> Cow<'a, str> {
if config.hyperlinks && atty::is(atty::Stream::Stdout) {
features::hyperlinks::format_commit_line_with_osc8_commit_hyperlink(line, config)
} else {
Expand Down
Loading

0 comments on commit 19eec9b

Please sign in to comment.