use anyhow::{Context, Result, bail};
use gix::{bstr::BString, config::AsKey};
use std::io::Write as _;
use crate::OutputFormat;
pub fn list_files(
repo: gix::Repository,
overrides: Vec<BString>,
format: OutputFormat,
mut out: impl std::io::Write,
) -> Result<()> {
if format != OutputFormat::Human {
bail!("Only human output format is supported at the moment");
}
let repo = gix::open_opts(repo.git_dir(), repo.open_options().clone().cli_overrides(overrides))?;
let config = repo.config_snapshot();
let mut seen = std::collections::BTreeSet::new();
for meta in config.sections().map(|section| section.meta()).chain([config.meta()]) {
let Some(path) = meta.path.as_ref() else {
continue;
};
if seen.insert(path) {
if meta.level == 0 {
writeln!(out, "{}\t{{ source={:?} }}", path.display(), meta.source)?;
} else {
writeln!(
out,
"{}\t{{ source={:?}, include-level={} }}",
path.display(),
meta.source,
meta.level
)?;
}
}
}
Ok(())
}
pub fn show(
repo: gix::Repository,
filters: Vec<BString>,
overrides: Vec<BString>,
format: OutputFormat,
mut out: impl std::io::Write,
) -> Result<()> {
if format != OutputFormat::Human {
bail!("Only human output format is supported at the moment");
}
let repo = gix::open_opts(repo.git_dir(), repo.open_options().clone().cli_overrides(overrides))?;
let config = repo.config_snapshot();
if let Some(frontmatter) = config.frontmatter() {
for event in frontmatter {
event.write_to(&mut out)?;
}
}
let filters: Vec<_> = filters.into_iter().map(Filter::new).collect();
let mut last_meta = None;
let mut it = config.sections_and_postmatter().peekable();
while let Some((section, matter)) = it.next() {
if !filters.is_empty() && !filters.iter().any(|filter| filter.matches_section(§ion)) {
continue;
}
let meta = section.meta();
if last_meta != Some(meta) {
write_meta(meta, &mut out)?;
}
last_meta = Some(meta);
section.write_to(&mut out)?;
for event in matter {
event.write_to(&mut out)?;
}
if it
.peek()
.is_some_and(|(next_section, _)| next_section.header().name() != section.header().name())
{
writeln!(&mut out)?;
}
}
Ok(())
}
pub fn fmt(
repo: Option<gix::Repository>,
in_file: Option<std::path::PathBuf>,
out_file: Option<std::path::PathBuf>,
in_place: bool,
mut out: impl std::io::Write,
) -> Result<()> {
if in_place && out_file.is_some() {
bail!("Cannot combine --in-place with an explicit output file");
}
let source = match in_file {
Some(path) => path,
None => repo
.context("Formatting the repository-local configuration requires being in a repository")?
.common_dir()
.join("config"),
};
let lock = in_place
.then(|| {
gix::lock::File::acquire_to_update_resource(&source, gix::lock::acquire::Fail::Immediately, None)
.with_context(|| format!("Could not lock configuration file at '{}'", source.display()))
})
.transpose()?;
let input = std::fs::read(&source)
.with_context(|| format!("Could not read configuration file at '{}'", source.display()))?;
let formatted = gix::config::format::normalize(&input, Default::default())?;
match (lock, out_file) {
(Some(mut lock), _) => {
lock.write_all(&formatted)
.with_context(|| format!("Could not write formatted configuration to '{}.lock'", source.display()))?;
lock.commit()
.map_err(|err| err.error)
.with_context(|| format!("Could not commit formatted configuration to '{}'", source.display()))?;
}
(None, Some(path)) => std::fs::write(&path, &formatted)
.with_context(|| format!("Could not write formatted configuration to '{}'", path.display()))?,
(None, None) => out.write_all(&formatted)?,
}
Ok(())
}
struct Filter {
name: String,
subsection: Option<BString>,
}
impl Filter {
fn new(input: BString) -> Self {
match (&input).try_as_key() {
Some(key) => Filter {
name: key.section_name.into(),
subsection: key.subsection_name.map(ToOwned::to_owned),
},
None => Filter {
name: input.to_string(),
subsection: None,
},
}
}
fn matches_section(&self, section: &gix::config::file::SectionRef<'_>) -> bool {
let ignore_case = gix::glob::wildmatch::Mode::IGNORE_CASE;
if !gix::glob::wildmatch(self.name.as_bytes().into(), section.header().name(), ignore_case) {
return false;
}
match (self.subsection.as_deref(), section.header().subsection_name()) {
(Some(filter), Some(name)) => {
if !gix::glob::wildmatch(filter.as_slice().into(), name, ignore_case) {
return false;
}
}
(None, _) => {}
(Some(_), None) => return false,
}
true
}
}
fn write_meta(meta: &gix::config::file::Metadata, out: &mut impl std::io::Write) -> std::io::Result<()> {
writeln!(
out,
"# From '{}' ({:?}{}{})",
meta.path
.as_deref()
.map_or_else(|| "memory".into(), |p| p.display().to_string()),
meta.source,
if meta.level != 0 {
format!(", include level {}", meta.level)
} else {
Default::default()
},
if meta.trust != gix::sec::Trust::Full {
", untrusted"
} else {
Default::default()
}
)
}