use anyhow::{Context, Result, bail}; use gix::{bstr::BString, config::AsKey}; use std::io::Write as _; use crate::OutputFormat; /// List all files which contributed sections to the resolved configuration, in precedence order. pub fn list_files( repo: gix::Repository, overrides: Vec, 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, overrides: Vec, 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(()) } /// Format the git configuration file at `in_file`, or the repository-local configuration if `in_file` /// is `None`, writing the result back in place, to `out_file`, or to `out` (stdout) respectively. pub fn fmt( repo: Option, in_file: Option, out_file: Option, 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, } 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() } ) }