mirror of https://github.com/sharkdp/bat.git
feat: Add git-blame style.
* Added a new decorator. * Added required changes in `config` in order to recognize 2 new CLI options/arguments. * Added a simple format ability with the placeholders from [git's pretty-formats](https://git-scm.com/docs/pretty-formats) * Updated doc tests to take into consideration new options.
This commit is contained in:
parent
f7bea6de5b
commit
ce9b212594
|
@ -138,6 +138,7 @@ Options:
|
|||
* rule: horizontal lines to delimit files.
|
||||
* numbers: show line numbers in the side bar.
|
||||
* snip: draw separation lines between distinct line ranges.
|
||||
* blame: show Git blame information.
|
||||
|
||||
-r, --line-range <N:M>
|
||||
Only print the specified range of lines for each file. For example:
|
||||
|
@ -154,6 +155,12 @@ Options:
|
|||
This option exists for POSIX-compliance reasons ('u' is for 'unbuffered'). The output is
|
||||
always unbuffered - this option is simply ignored.
|
||||
|
||||
--blame-format <format>
|
||||
Set the format for the Git blame output. The format string can contain placeholders like
|
||||
'%h' (abbreviated commit hash), '%an' (author name), '%H' (full commit hash), '%s'
|
||||
(summary), '%d' (ref names), '%m' (message), '%ae' (author email), '%cn' (committer name),
|
||||
'%ce' (committer email)
|
||||
|
||||
--diagnostic
|
||||
Show diagnostic information for bug reports.
|
||||
|
||||
|
|
|
@ -45,11 +45,13 @@ Options:
|
|||
Display all supported highlighting themes.
|
||||
--style <components>
|
||||
Comma-separated list of style elements to display (*default*, auto, full, plain, changes,
|
||||
header, header-filename, header-filesize, grid, rule, numbers, snip).
|
||||
header, header-filename, header-filesize, grid, rule, numbers, snip, blame).
|
||||
-r, --line-range <N:M>
|
||||
Only print the lines from N to M.
|
||||
-L, --list-languages
|
||||
Display all supported languages.
|
||||
--blame-format <format>
|
||||
Specify a format for the git-blame content.
|
||||
-h, --help
|
||||
Print help (see more with '--help')
|
||||
-V, --version
|
||||
|
|
|
@ -289,6 +289,12 @@ impl App {
|
|||
use_custom_assets: !self.matches.get_flag("no-custom-assets"),
|
||||
#[cfg(feature = "lessopen")]
|
||||
use_lessopen: self.matches.get_flag("lessopen"),
|
||||
#[cfg(feature = "git")]
|
||||
blame_format: self
|
||||
.matches
|
||||
.get_one::<String>("blame-format")
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| String::from("%h: %an <%ae>")),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -411,6 +411,8 @@ pub fn build_app(interactive_output: bool) -> Command {
|
|||
"snip",
|
||||
#[cfg(feature = "git")]
|
||||
"changes",
|
||||
#[cfg(feature = "git")]
|
||||
"blame",
|
||||
].contains(style)
|
||||
});
|
||||
|
||||
|
@ -422,7 +424,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
|||
})
|
||||
.help(
|
||||
"Comma-separated list of style elements to display \
|
||||
(*default*, auto, full, plain, changes, header, header-filename, header-filesize, grid, rule, numbers, snip).",
|
||||
(*default*, auto, full, plain, changes, header, header-filename, header-filesize, grid, rule, numbers, snip, blame).",
|
||||
)
|
||||
.long_help(
|
||||
"Configure which elements (line numbers, file headers, grid \
|
||||
|
@ -445,7 +447,8 @@ pub fn build_app(interactive_output: bool) -> Command {
|
|||
and the header from the content.\n \
|
||||
* rule: horizontal lines to delimit files.\n \
|
||||
* numbers: show line numbers in the side bar.\n \
|
||||
* snip: draw separation lines between distinct line ranges.",
|
||||
* snip: draw separation lines between distinct line ranges.\n \
|
||||
* blame: show Git blame information.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
|
@ -520,6 +523,23 @@ pub fn build_app(interactive_output: bool) -> Command {
|
|||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
{
|
||||
app = app
|
||||
.arg(
|
||||
Arg::new("blame-format")
|
||||
.long("blame-format")
|
||||
.value_name("format")
|
||||
.help("Specify a format for the git-blame content.")
|
||||
.long_help(
|
||||
"Set the format for the Git blame output. The format string \
|
||||
can contain placeholders like '%h' (abbreviated commit hash), '%an' (author name), \
|
||||
'%H' (full commit hash), '%s' (summary), '%d' (ref names), '%m' (message), \
|
||||
'%ae' (author email), '%cn' (committer name), '%ce' (committer email)",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
app = app
|
||||
.arg(
|
||||
Arg::new("config-file")
|
||||
|
|
|
@ -94,6 +94,10 @@ pub struct Config<'a> {
|
|||
// Whether or not to use $LESSOPEN if set
|
||||
#[cfg(feature = "lessopen")]
|
||||
pub use_lessopen: bool,
|
||||
|
||||
// Format for the git blame column
|
||||
#[cfg(feature = "git")]
|
||||
pub blame_format: String,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "minimal-application", feature = "paging"))]
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use std::io::{self, BufRead, Write};
|
||||
|
||||
use crate::assets::HighlightingAssets;
|
||||
use crate::config::{Config, VisibleLines};
|
||||
#[cfg(feature = "git")]
|
||||
use crate::diff::{get_git_diff, LineChanges};
|
||||
use crate::diff::{get_git_diff, LineChanges, get_blame_file};
|
||||
use crate::error::*;
|
||||
use crate::input::{Input, InputReader, OpenedInput};
|
||||
#[cfg(feature = "lessopen")]
|
||||
|
@ -174,6 +173,30 @@ impl<'b> Controller<'b> {
|
|||
None
|
||||
};
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
let line_blames = if !self.config.loop_through && self.config.style_components.blame()
|
||||
{
|
||||
match opened_input.kind {
|
||||
crate::input::OpenedInputKind::OrdinaryFile(ref path) => {
|
||||
let blame_format = self.config.blame_format.clone();
|
||||
let blames = get_blame_file(path, &blame_format);
|
||||
|
||||
// Skip files without Git modifications
|
||||
if blames
|
||||
.as_ref()
|
||||
.map(|changes| changes.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
blames
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut printer: Box<dyn Printer> = if self.config.loop_through {
|
||||
Box::new(SimplePrinter::new(self.config))
|
||||
} else {
|
||||
|
@ -183,6 +206,8 @@ impl<'b> Controller<'b> {
|
|||
&mut opened_input,
|
||||
#[cfg(feature = "git")]
|
||||
&line_changes,
|
||||
#[cfg(feature = "git")]
|
||||
&line_blames,
|
||||
)?)
|
||||
};
|
||||
|
||||
|
@ -222,11 +247,9 @@ impl<'b> Controller<'b> {
|
|||
.push(LineRange::new(line.saturating_sub(context), line + context));
|
||||
}
|
||||
}
|
||||
|
||||
LineRanges::from(line_ranges)
|
||||
}
|
||||
};
|
||||
|
||||
self.print_file_ranges(printer, writer, &mut input.reader, &line_ranges)?;
|
||||
}
|
||||
printer.print_footer(writer, input)?;
|
||||
|
@ -268,7 +291,6 @@ impl<'b> Controller<'b> {
|
|||
printer.print_snip(writer)?;
|
||||
}
|
||||
}
|
||||
|
||||
printer.print_line(false, writer, line_number, &line_buffer)?;
|
||||
}
|
||||
RangeCheckResult::AfterLastRange => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#[cfg(feature = "git")]
|
||||
use crate::diff::LineChange;
|
||||
use crate::diff::{LineChange};
|
||||
use crate::printer::{Colors, InteractivePrinter};
|
||||
use nu_ansi_term::Style;
|
||||
|
||||
|
@ -127,6 +127,63 @@ impl Decoration for LineChangesDecoration {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
pub(crate) struct LineBlamesDecoration {
|
||||
color: Style,
|
||||
max_length: usize,
|
||||
}
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
impl LineBlamesDecoration {
|
||||
#[inline]
|
||||
fn generate_cached(style: Style, text: &str, length: usize) -> DecorationText {
|
||||
DecorationText {
|
||||
text: style.paint(text).to_string(),
|
||||
width: length,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new(colors: &Colors, max_length: usize) -> Self {
|
||||
LineBlamesDecoration {
|
||||
color: colors.git_blame,
|
||||
max_length: max_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
impl Decoration for LineBlamesDecoration {
|
||||
fn generate(
|
||||
&self,
|
||||
line_number: usize,
|
||||
continuation: bool,
|
||||
printer: &InteractivePrinter,
|
||||
) -> DecorationText {
|
||||
if !continuation {
|
||||
if let Some(ref changes) = printer.line_blames {
|
||||
let result = changes.get(&(line_number as u32));
|
||||
if let Some(result) = result {
|
||||
let length = self.width();
|
||||
if result.len() < length {
|
||||
return Self::generate_cached(
|
||||
self.color,
|
||||
format!("{: <width$}", result, width = length).as_str(),
|
||||
length,
|
||||
);
|
||||
}
|
||||
return Self::generate_cached(self.color, result, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::generate_cached(Style::default(), " ", self.width())
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.max_length
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct GridBorderDecoration {
|
||||
cached: DecorationText,
|
||||
}
|
||||
|
|
66
src/diff.rs
66
src/diff.rs
|
@ -3,8 +3,7 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use git2::{DiffOptions, IntoCString, Repository};
|
||||
use git2::{Commit, BlameHunk, Blame, BlameOptions, DiffOptions, IntoCString, Repository};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum LineChange {
|
||||
|
@ -15,6 +14,7 @@ pub enum LineChange {
|
|||
}
|
||||
|
||||
pub type LineChanges = HashMap<u32, LineChange>;
|
||||
pub type LineBlames = HashMap<u32, String>;
|
||||
|
||||
pub fn get_git_diff(filename: &Path) -> Option<LineChanges> {
|
||||
let repo = Repository::discover(filename).ok()?;
|
||||
|
@ -81,3 +81,65 @@ pub fn get_git_diff(filename: &Path) -> Option<LineChanges> {
|
|||
|
||||
Some(line_changes)
|
||||
}
|
||||
|
||||
pub fn get_blame_line(blame: &Blame, filename: &Path, line: u32, blame_format: &str) -> Option<String> {
|
||||
let repo = Repository::discover(filename).ok()?;
|
||||
let default_return = "Unknown".to_string();
|
||||
let diff = get_git_diff(filename).unwrap();
|
||||
if diff.contains_key(&line) {
|
||||
return Some(format!("{} <{}>", default_return, default_return));
|
||||
}
|
||||
if let Some(line_blame) = blame.get_line(line as usize) {
|
||||
let signature = line_blame.final_signature();
|
||||
let name = signature.name().unwrap_or(default_return.as_str());
|
||||
let email = signature.email().unwrap_or(default_return.as_str());
|
||||
if blame_format.is_empty() {
|
||||
return Some(format!("{} <{}>", name, email));
|
||||
}
|
||||
let commit_id = line_blame.final_commit_id();
|
||||
let commit = repo.find_commit(commit_id).ok()?;
|
||||
|
||||
return Some(format_blame(&line_blame, &commit, blame_format));
|
||||
}
|
||||
Some(default_return)
|
||||
}
|
||||
|
||||
|
||||
pub fn format_blame(blame_hunk: &BlameHunk, commit: &Commit, blame_format: &str) -> String {
|
||||
let mut result = String::from(blame_format);
|
||||
let abbreviated_id_buf = commit.as_object().short_id();
|
||||
let abbreviated_id = abbreviated_id_buf.as_ref().ok().map(|id| id.as_str()).unwrap_or(Some(""));
|
||||
let signature = blame_hunk.final_signature();
|
||||
result = result.replace("%an", signature.name().unwrap_or("Unknown"));
|
||||
result = result.replace("%ae", signature.email().unwrap_or("Unknown"));
|
||||
result = result.replace("%H", commit.id().to_string().as_str());
|
||||
result = result.replace("%h", abbreviated_id.unwrap());
|
||||
result = result.replace("%s", commit.summary().unwrap_or("Unknown"));
|
||||
result = result.replace("%cn", commit.author().name().unwrap_or("Unknown"));
|
||||
result = result.replace("%ce", commit.author().email().unwrap_or("Unknown"));
|
||||
result = result.replace("%b", commit.message().unwrap_or("Unknown"));
|
||||
result = result.replace("%N", commit.parents().len().to_string().as_str());
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn get_blame_file(filename: &Path, blame_format: &str) -> Option<LineBlames> {
|
||||
let lines_in_file = fs::read_to_string(filename).ok()?.lines().count();
|
||||
let mut result = LineBlames::new();
|
||||
let mut blame_options = BlameOptions::new();
|
||||
let repo = Repository::discover(filename).ok()?;
|
||||
let repo_path_absolute = fs::canonicalize(repo.workdir()?).ok()?;
|
||||
let filepath_absolute = fs::canonicalize(filename).ok()?;
|
||||
let filepath_relative_to_repo = filepath_absolute.strip_prefix(&repo_path_absolute).ok()?;
|
||||
|
||||
let blame = repo.blame_file(
|
||||
filepath_relative_to_repo,
|
||||
Some(&mut blame_options),
|
||||
).ok()?;
|
||||
for i in 0..lines_in_file {
|
||||
if let Some(str_result) = get_blame_line(&blame, filename, i as u32, blame_format) {
|
||||
result.insert(i as u32, str_result);
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
}
|
|
@ -23,10 +23,11 @@ use unicode_width::UnicodeWidthChar;
|
|||
use crate::assets::{HighlightingAssets, SyntaxReferenceInSet};
|
||||
use crate::config::Config;
|
||||
#[cfg(feature = "git")]
|
||||
use crate::decorations::LineChangesDecoration;
|
||||
use crate::decorations::{LineChangesDecoration, LineBlamesDecoration};
|
||||
#[cfg(feature = "git")]
|
||||
use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration};
|
||||
#[cfg(feature = "git")]
|
||||
use crate::diff::LineChanges;
|
||||
use crate::diff::{LineChanges, LineBlames};
|
||||
use crate::error::*;
|
||||
use crate::input::OpenedInput;
|
||||
use crate::line_range::RangeCheckResult;
|
||||
|
@ -156,6 +157,8 @@ pub(crate) struct InteractivePrinter<'a> {
|
|||
content_type: Option<ContentType>,
|
||||
#[cfg(feature = "git")]
|
||||
pub line_changes: &'a Option<LineChanges>,
|
||||
#[cfg(feature = "git")]
|
||||
pub line_blames: &'a Option<LineBlames>,
|
||||
highlighter_from_set: Option<HighlighterFromSet<'a>>,
|
||||
background_color_highlight: Option<Color>,
|
||||
}
|
||||
|
@ -166,6 +169,7 @@ impl<'a> InteractivePrinter<'a> {
|
|||
assets: &'a HighlightingAssets,
|
||||
input: &mut OpenedInput,
|
||||
#[cfg(feature = "git")] line_changes: &'a Option<LineChanges>,
|
||||
#[cfg(feature = "git")] line_blames: &'a Option<LineBlames>,
|
||||
) -> Result<Self> {
|
||||
let theme = assets.get_theme(&config.theme);
|
||||
|
||||
|
@ -180,6 +184,17 @@ impl<'a> InteractivePrinter<'a> {
|
|||
// Create decorations.
|
||||
let mut decorations: Vec<Box<dyn Decoration>> = Vec::new();
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
{
|
||||
if config.style_components.blame() {
|
||||
let longest_key = line_blames
|
||||
.as_ref()
|
||||
.map(|blames| blames.values().map(|s| s.len()).max().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
decorations.push(Box::new(LineBlamesDecoration::new(&colors, longest_key)));
|
||||
}
|
||||
}
|
||||
|
||||
if config.style_components.numbers() {
|
||||
decorations.push(Box::new(LineNumberDecoration::new(&colors)));
|
||||
}
|
||||
|
@ -239,6 +254,8 @@ impl<'a> InteractivePrinter<'a> {
|
|||
ansi_style: AnsiStyle::new(),
|
||||
#[cfg(feature = "git")]
|
||||
line_changes,
|
||||
#[cfg(feature = "git")]
|
||||
line_blames,
|
||||
highlighter_from_set,
|
||||
background_color_highlight,
|
||||
})
|
||||
|
@ -762,6 +779,7 @@ pub struct Colors {
|
|||
pub git_added: Style,
|
||||
pub git_removed: Style,
|
||||
pub git_modified: Style,
|
||||
pub git_blame: Style,
|
||||
pub line_number: Style,
|
||||
}
|
||||
|
||||
|
@ -791,6 +809,7 @@ impl Colors {
|
|||
git_added: Green.normal(),
|
||||
git_removed: Red.normal(),
|
||||
git_modified: Yellow.normal(),
|
||||
git_blame: Red.normal(),
|
||||
line_number: gutter_style,
|
||||
}
|
||||
}
|
||||
|
|
11
src/style.rs
11
src/style.rs
|
@ -9,6 +9,8 @@ pub enum StyleComponent {
|
|||
Auto,
|
||||
#[cfg(feature = "git")]
|
||||
Changes,
|
||||
#[cfg(feature = "git")]
|
||||
Blame,
|
||||
Grid,
|
||||
Rule,
|
||||
Header,
|
||||
|
@ -33,6 +35,8 @@ impl StyleComponent {
|
|||
}
|
||||
#[cfg(feature = "git")]
|
||||
StyleComponent::Changes => &[StyleComponent::Changes],
|
||||
#[cfg(feature = "git")]
|
||||
StyleComponent::Blame => &[StyleComponent::Blame],
|
||||
StyleComponent::Grid => &[StyleComponent::Grid],
|
||||
StyleComponent::Rule => &[StyleComponent::Rule],
|
||||
StyleComponent::Header => &[StyleComponent::HeaderFilename],
|
||||
|
@ -70,6 +74,8 @@ impl FromStr for StyleComponent {
|
|||
"auto" => Ok(StyleComponent::Auto),
|
||||
#[cfg(feature = "git")]
|
||||
"changes" => Ok(StyleComponent::Changes),
|
||||
#[cfg(feature = "git")]
|
||||
"blame" => Ok(StyleComponent::Blame),
|
||||
"grid" => Ok(StyleComponent::Grid),
|
||||
"rule" => Ok(StyleComponent::Rule),
|
||||
"header" => Ok(StyleComponent::Header),
|
||||
|
@ -98,6 +104,11 @@ impl StyleComponents {
|
|||
self.0.contains(&StyleComponent::Changes)
|
||||
}
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
pub fn blame(&self) -> bool {
|
||||
self.0.contains(&StyleComponent::Blame)
|
||||
}
|
||||
|
||||
pub fn grid(&self) -> bool {
|
||||
self.0.contains(&StyleComponent::Grid)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue