Make --show-config display all supported config keys

Include settings for disabled features, paint additional styles and decorations, and preserve hunk-header attributes.

Run tests from the repository root:
cargo test subcommands::show_config::tests
cargo test

Checks: cargo fmt --all --check; cargo clippy -- -D warnings
This commit is contained in:
Dan Davison
2026-10-04 19:44:30 -04:00
parent 1facec5bf7
commit 83284d7edd
2 changed files with 363 additions and 31 deletions
+3 -5
View File
@@ -136,15 +136,13 @@ pub fn run_app(
return Ok(0);
};
let _show_config = opt.show_config;
let config = config::Config::from(opt);
if _show_config {
if opt.show_config {
let stdout = io::stdout();
let mut stdout = stdout.lock();
subcommands::show_config::show_config(&config, &mut stdout)?;
subcommands::show_config::show_config(opt, &mut stdout)?;
return Ok(0);
}
let config = config::Config::from(opt);
// The following block structure is because of `writer` and related lifetimes:
let pager_cfg = (&config).into();
+360 -26
View File
@@ -1,5 +1,6 @@
use std::io::Write;
use clap::ValueEnum;
use itertools::Itertools;
use crate::cli;
@@ -10,7 +11,72 @@ use crate::paint::BgFillMethod;
use crate::style;
use crate::utils::bat::output::PagingMode;
pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::Result<()> {
pub fn show_config(opt: cli::Opt, writer: &mut dyn Write) -> std::io::Result<()> {
let options = option_values(&opt);
let config = config::Config::from(opt);
let mut output = Vec::new();
write_config(&config, &mut output)?;
write_additional_styles(&config, &mut output)?;
write_decoration_styles(&config, &mut output)?;
write_additional_values(&config, &mut output)?;
for (key, value) in options {
writeln!(output, " {key:29} = {value}")?;
}
for line in output.split_inclusive(|&byte| byte == b'\n').sorted() {
writer.write_all(line)?;
}
Ok(())
}
fn option_values(opt: &cli::Opt) -> Vec<(String, String)> {
let mut options = Vec::new();
macro_rules! values {
($($field:ident),* $(,)?) => {
$(options.push((stringify!($field).replace('_', "-"), opt.$field.to_string()));)*
};
}
macro_rules! strings {
($($field:ident),* $(,)?) => {
$(options.push((stringify!($field).replace('_', "-"), format_option_value(&opt.$field)));)*
};
}
macro_rules! optional_strings {
($($field:ident),* $(,)?) => {
$(options.push((stringify!($field).replace('_', "-"), format_option_value(opt.$field.as_deref().unwrap_or(""))));)*
};
}
values!(
dark,
light,
diff_highlight,
diff_so_fancy,
no_gitconfig,
raw
);
strings!(
config,
blame_separator_format,
wrap_max_lines,
wrap_right_percent,
);
optional_strings!(features, map_styles);
options.push((
"file-transformation".to_string(),
format_option_value(opt.file_regex_replacement.as_deref().unwrap_or("")),
));
options.push((
"detect-dark-light".to_string(),
opt.detect_dark_light
.to_possible_value()
.unwrap()
.get_name()
.to_string(),
));
options
}
fn write_config(config: &config::Config, writer: &mut dyn Write) -> std::io::Result<()> {
// styles first
writeln!(
writer,
@@ -37,7 +103,34 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
.join(" "),
commit_style = config.commit_style.to_painted_string(),
file_style = config.file_style.to_painted_string(),
hunk_header_style = config.hunk_header_style.to_painted_string(),
hunk_header_style = config.hunk_header_style.paint(format!(
"{}{}{}{}",
if matches!(
config.hunk_header_style_include_file_path,
config::HunkHeaderIncludeFilePath::Yes
) {
"file "
} else {
""
},
if matches!(
config.hunk_header_style_include_line_number,
config::HunkHeaderIncludeLineNumber::Yes
) {
"line-number "
} else {
""
},
if matches!(
config.hunk_header_style_include_code_fragment,
config::HunkHeaderIncludeCodeFragment::No
) {
"omit-code-fragment "
} else {
""
},
config.hunk_header_style,
)),
minus_emph_style = config.minus_emph_style.to_painted_string(),
minus_empty_line_marker_style = config.minus_empty_line_marker_style.to_painted_string(),
minus_non_emph_style = config.minus_non_emph_style.to_painted_string(),
@@ -72,13 +165,11 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
" hyperlinks = {hyperlinks}",
hyperlinks = config.hyperlinks
)?;
if config.hyperlinks {
writeln!(
writer,
" hyperlinks-file-link-format = {hyperlinks_file_link_format}",
hyperlinks_file_link_format = format_option_value(&config.hyperlinks_file_link_format),
)?
}
writeln!(
writer,
" hyperlinks-file-link-format = {hyperlinks_file_link_format}",
hyperlinks_file_link_format = format_option_value(&config.hyperlinks_file_link_format),
)?;
writeln!(
writer,
" inspect-raw-lines = {inspect_raw_lines}
@@ -94,27 +185,23 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
" line-numbers = {line_numbers}",
line_numbers = config.line_numbers
)?;
if config.line_numbers {
writeln!(
writer,
" line-numbers-minus-style = {line_numbers_minus_style}
writeln!(
writer,
" line-numbers-minus-style = {line_numbers_minus_style}
line-numbers-zero-style = {line_numbers_zero_style}
line-numbers-plus-style = {line_numbers_plus_style}
line-numbers-left-style = {line_numbers_left_style}
line-numbers-right-style = {line_numbers_right_style}
line-numbers-left-format = {line_numbers_left_format}
line-numbers-right-format = {line_numbers_right_format}",
line_numbers_minus_style =
config.line_numbers_style_minusplus[Minus].to_painted_string(),
line_numbers_zero_style = config.line_numbers_zero_style.to_painted_string(),
line_numbers_plus_style = config.line_numbers_style_minusplus[Plus].to_painted_string(),
line_numbers_left_style = config.line_numbers_style_leftright[Left].to_painted_string(),
line_numbers_right_style =
config.line_numbers_style_leftright[Right].to_painted_string(),
line_numbers_left_format = format_option_value(&config.line_numbers_format[Left]),
line_numbers_right_format = format_option_value(&config.line_numbers_format[Right]),
)?
}
line_numbers_minus_style = config.line_numbers_style_minusplus[Minus].to_painted_string(),
line_numbers_zero_style = config.line_numbers_zero_style.to_painted_string(),
line_numbers_plus_style = config.line_numbers_style_minusplus[Plus].to_painted_string(),
line_numbers_left_style = config.line_numbers_style_leftright[Left].to_painted_string(),
line_numbers_right_style = config.line_numbers_style_leftright[Right].to_painted_string(),
line_numbers_left_format = format_option_value(&config.line_numbers_format[Left]),
line_numbers_right_format = format_option_value(&config.line_numbers_format[Right]),
)?;
writeln!(
writer,
" max-line-distance = {max_line_distance}
@@ -165,6 +252,192 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
Ok(())
}
fn write_additional_styles(config: &config::Config, writer: &mut dyn Write) -> std::io::Result<()> {
for (key, style) in [
("blame-code-style", config.blame_code_style),
("blame-separator-style", config.blame_separator_style),
(
"grep-context-line-style",
Some(config.grep_context_line_style),
),
(
"grep-header-file-style",
Some(config.classic_grep_header_file_style),
),
("grep-match-line-style", Some(config.grep_match_line_style)),
("grep-match-word-style", Some(config.grep_match_word_style)),
(
"hunk-header-file-style",
Some(config.hunk_header_file_style),
),
(
"hunk-header-line-number-style",
Some(config.hunk_header_line_number_style),
),
("inline-hint-style", Some(config.inline_hint_style)),
(
"merge-conflict-ours-diff-header-style",
Some(config.merge_conflict_ours_diff_header_style),
),
(
"merge-conflict-theirs-diff-header-style",
Some(config.merge_conflict_theirs_diff_header_style),
),
] {
let value = style
.map(|style| style.to_painted_string().to_string())
.unwrap_or_else(|| "''".to_string());
writeln!(writer, " {key:29} = {value}")?;
}
Ok(())
}
fn write_decoration_styles(config: &config::Config, writer: &mut dyn Write) -> std::io::Result<()> {
let grep_header_style = match config.grep_output_type {
Some(config::GrepType::Ripgrep) => config.ripgrep_header_style,
_ => config.classic_grep_header_style,
};
for (key, style) in [
(
"commit-decoration-style",
config.commit_style.decoration_style,
),
("file-decoration-style", config.file_style.decoration_style),
(
"hunk-header-decoration-style",
config.hunk_header_style.decoration_style,
),
(
"grep-header-decoration-style",
grep_header_style.decoration_style,
),
(
"merge-conflict-ours-diff-header-decoration-style",
config
.merge_conflict_ours_diff_header_style
.decoration_style,
),
(
"merge-conflict-theirs-diff-header-decoration-style",
config
.merge_conflict_theirs_diff_header_style
.decoration_style,
),
] {
writeln!(writer, " {key:29} = {}", painted_decoration_style(style))?;
}
Ok(())
}
fn write_additional_values(config: &config::Config, writer: &mut dyn Write) -> std::io::Result<()> {
for (key, value) in [
("blame-format", config.blame_format.as_str()),
(
"blame-timestamp-format",
config.blame_timestamp_format.as_str(),
),
(
"blame-timestamp-output-format",
config
.blame_timestamp_output_format
.as_deref()
.unwrap_or(""),
),
("default-language", config.default_language.as_str()),
("diff-args", config.diff_args.as_str()),
("file-copied-label", config.file_copied_label.as_str()),
(
"grep-separator-symbol",
config.grep_separator_symbol.as_str(),
),
(
"grep-output-type",
match config.grep_output_type {
Some(config::GrepType::Classic) => "classic",
Some(config::GrepType::Ripgrep) => "ripgrep",
None => "",
},
),
("hunk-label", config.hunk_label.as_str()),
(
"hyperlinks-commit-link-format",
config
.hyperlinks_commit_link_format
.as_deref()
.unwrap_or(""),
),
(
"merge-conflict-begin-symbol",
config.merge_conflict_begin_symbol.as_str(),
),
(
"merge-conflict-end-symbol",
config.merge_conflict_end_symbol.as_str(),
),
] {
writeln!(writer, " {key:29} = {}", format_option_value(value))?;
}
for (key, value) in [
("wrap-left-symbol", config.wrap_config.left_symbol.as_str()),
(
"wrap-right-symbol",
config.wrap_config.right_symbol.as_str(),
),
(
"wrap-right-prefix-symbol",
config.wrap_config.right_prefix_symbol.as_str(),
),
] {
writeln!(
writer,
" {key:29} = {}",
config.inline_hint_style.paint(format_option_value(value))
)?;
}
writeln!(
writer,
" {:29} = {}",
"commit-regex",
format_option_value(config.commit_regex.as_str())
)?;
writeln!(writer, " {:29} = {}", "color-only", config.color_only)?;
writeln!(
writer,
" {:29} = {}",
"relative-paths", config.relative_paths
)?;
writeln!(
writer,
" {:29} = {}",
"line-buffer-size", config.line_buffer_size
)?;
writeln!(
writer,
" {:29} = {}",
"max-syntax-highlighting-length", config.max_syntax_length
)?;
Ok(())
}
fn painted_decoration_style(decoration: style::DecorationStyle) -> String {
use style::DecorationStyle::*;
let (ansi_term_style, attributes) = match decoration {
NoDecoration => return "none".to_string(),
Box(style) => (style, "box"),
Underline(style) => (style, "ul"),
Overline(style) => (style, "ol"),
UnderOverline(style) => (style, "ul ol"),
BoxWithUnderline(style) => (style, "box ul"),
BoxWithOverline(style) => (style, "box ol"),
BoxWithUnderOverline(style) => (style, "box ul ol"),
};
let style = style::Style {
ansi_term_style,
..style::Style::default()
};
style.paint(format!("{style} {attributes}")).to_string()
}
// Heuristics determining whether to quote string option values when printing values intended for
// git config.
fn format_option_value<S>(s: S) -> String
@@ -185,6 +458,8 @@ where
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use crate::tests::integration_test_utils;
use super::*;
@@ -193,9 +468,9 @@ mod tests {
#[test]
fn test_show_config() {
let config = integration_test_utils::make_config_from_args(&[]);
let opt = integration_test_utils::make_options_from_args(&[]);
let mut writer = Cursor::new(vec![0; 1024]);
show_config(&config, &mut writer).unwrap();
show_config(opt, &mut writer).unwrap();
let mut s = String::new();
writer.rewind().unwrap();
writer.read_to_string(&mut s).unwrap();
@@ -203,4 +478,63 @@ mod tests {
assert!(s.contains(" commit-style = raw\n"));
assert!(s.contains(r" word-diff-regex = '\w+'"));
}
#[test]
fn test_show_config_includes_all_config_keys() {
let opt = integration_test_utils::make_options_from_args(&[]);
let mut writer = Vec::new();
show_config(opt, &mut writer).unwrap();
let output = String::from_utf8(writer).unwrap();
let output = ansi::strip_ansi_codes(&output);
let keys: Vec<_> = output
.lines()
.map(|line| line.split_once('=').unwrap().0.trim())
.collect();
let actual: BTreeSet<_> = keys.iter().copied().collect();
assert_eq!(keys.len(), actual.len(), "duplicate config keys");
let names = cli::Opt::get_argument_and_option_names();
let expected: BTreeSet<_> = names
.values()
.map(String::as_str)
.filter(|name| {
!matches!(
*name,
"24-bit-color" | "parse-ansi" | "show-colors" | "show-themes"
)
})
.collect();
assert_eq!(actual, expected);
assert!(
keys.windows(2).all(|pair| pair[0] < pair[1]),
"config keys are not sorted alphabetically"
);
}
#[test]
fn test_show_config_paints_decoration_and_additional_styles() {
let opt = integration_test_utils::make_options_from_args(&[
"--file-decoration-style=bold red blue box ul ol",
"--grep-context-line-style=italic green",
"--hunk-header-style=file line-number omit-code-fragment green",
]);
let mut writer = Vec::new();
show_config(opt, &mut writer).unwrap();
let output = String::from_utf8(writer).unwrap();
for (key, value) in [
("file-decoration-style", "bold red blue box ul ol"),
("grep-context-line-style", "italic green"),
(
"hunk-header-style",
"file line-number omit-code-fragment green",
),
] {
let line = output
.lines()
.find(|line| line.trim_start().starts_with(key))
.unwrap();
let printed_value = line.split_once('=').unwrap().1.trim();
assert_eq!(ansi::strip_ansi_codes(printed_value), value);
assert!(printed_value.contains('\x1b'), "{} is not painted", key);
}
}
}