Skip to main content

gitoxide_core/repository/
config.rs

1use anyhow::{Context, Result, bail};
2use gix::{bstr::BString, config::AsKey};
3use std::io::Write as _;
4
5use crate::OutputFormat;
6
7/// List all files which contributed sections to the resolved configuration, in precedence order.
8pub fn list_files(
9    repo: gix::Repository,
10    overrides: Vec<BString>,
11    format: OutputFormat,
12    mut out: impl std::io::Write,
13) -> Result<()> {
14    if format != OutputFormat::Human {
15        bail!("Only human output format is supported at the moment");
16    }
17    let repo = gix::open_opts(repo.git_dir(), repo.open_options().clone().cli_overrides(overrides))?;
18    let config = repo.config_snapshot();
19    let mut seen = std::collections::BTreeSet::new();
20    for meta in config.sections().map(|section| section.meta()).chain([config.meta()]) {
21        let Some(path) = meta.path.as_ref() else {
22            continue;
23        };
24        if seen.insert(path) {
25            if meta.level == 0 {
26                writeln!(out, "{}\t{{ source={:?} }}", path.display(), meta.source)?;
27            } else {
28                writeln!(
29                    out,
30                    "{}\t{{ source={:?}, include-level={} }}",
31                    path.display(),
32                    meta.source,
33                    meta.level
34                )?;
35            }
36        }
37    }
38    Ok(())
39}
40
41pub fn show(
42    repo: gix::Repository,
43    filters: Vec<BString>,
44    overrides: Vec<BString>,
45    format: OutputFormat,
46    mut out: impl std::io::Write,
47) -> Result<()> {
48    if format != OutputFormat::Human {
49        bail!("Only human output format is supported at the moment");
50    }
51    let repo = gix::open_opts(repo.git_dir(), repo.open_options().clone().cli_overrides(overrides))?;
52    let config = repo.config_snapshot();
53    if let Some(frontmatter) = config.frontmatter() {
54        for event in frontmatter {
55            event.write_to(&mut out)?;
56        }
57    }
58    let filters: Vec<_> = filters.into_iter().map(Filter::new).collect();
59    let mut last_meta = None;
60    let mut it = config.sections_and_postmatter().peekable();
61    while let Some((section, matter)) = it.next() {
62        if !filters.is_empty() && !filters.iter().any(|filter| filter.matches_section(&section)) {
63            continue;
64        }
65
66        let meta = section.meta();
67        if last_meta != Some(meta) {
68            write_meta(meta, &mut out)?;
69        }
70        last_meta = Some(meta);
71
72        section.write_to(&mut out)?;
73        for event in matter {
74            event.write_to(&mut out)?;
75        }
76        if it
77            .peek()
78            .is_some_and(|(next_section, _)| next_section.header().name() != section.header().name())
79        {
80            writeln!(&mut out)?;
81        }
82    }
83    Ok(())
84}
85
86/// Format the git configuration file at `in_file`, or the repository-local configuration if `in_file`
87/// is `None`, writing the result back in place, to `out_file`, or to `out` (stdout) respectively.
88pub fn fmt(
89    repo: Option<gix::Repository>,
90    in_file: Option<std::path::PathBuf>,
91    out_file: Option<std::path::PathBuf>,
92    in_place: bool,
93    mut out: impl std::io::Write,
94) -> Result<()> {
95    if in_place && out_file.is_some() {
96        bail!("Cannot combine --in-place with an explicit output file");
97    }
98    let source = match in_file {
99        Some(path) => path,
100        None => repo
101            .context("Formatting the repository-local configuration requires being in a repository")?
102            .common_dir()
103            .join("config"),
104    };
105    let lock = in_place
106        .then(|| {
107            gix::lock::File::acquire_to_update_resource(&source, gix::lock::acquire::Fail::Immediately, None)
108                .with_context(|| format!("Could not lock configuration file at '{}'", source.display()))
109        })
110        .transpose()?;
111    let input = std::fs::read(&source)
112        .with_context(|| format!("Could not read configuration file at '{}'", source.display()))?;
113    let formatted = gix::config::format::normalize(&input, Default::default())?;
114    match (lock, out_file) {
115        (Some(mut lock), _) => {
116            lock.write_all(&formatted)
117                .with_context(|| format!("Could not write formatted configuration to '{}.lock'", source.display()))?;
118            lock.commit()
119                .map_err(|err| err.error)
120                .with_context(|| format!("Could not commit formatted configuration to '{}'", source.display()))?;
121        }
122        (None, Some(path)) => std::fs::write(&path, &formatted)
123            .with_context(|| format!("Could not write formatted configuration to '{}'", path.display()))?,
124        (None, None) => out.write_all(&formatted)?,
125    }
126    Ok(())
127}
128
129struct Filter {
130    name: String,
131    subsection: Option<BString>,
132}
133
134impl Filter {
135    fn new(input: BString) -> Self {
136        match (&input).try_as_key() {
137            Some(key) => Filter {
138                name: key.section_name.into(),
139                subsection: key.subsection_name.map(ToOwned::to_owned),
140            },
141            None => Filter {
142                name: input.to_string(),
143                subsection: None,
144            },
145        }
146    }
147
148    fn matches_section(&self, section: &gix::config::file::SectionRef<'_>) -> bool {
149        let ignore_case = gix::glob::wildmatch::Mode::IGNORE_CASE;
150
151        if !gix::glob::wildmatch(self.name.as_bytes().into(), section.header().name(), ignore_case) {
152            return false;
153        }
154        match (self.subsection.as_deref(), section.header().subsection_name()) {
155            (Some(filter), Some(name)) => {
156                if !gix::glob::wildmatch(filter.as_slice().into(), name, ignore_case) {
157                    return false;
158                }
159            }
160            (None, _) => {}
161            (Some(_), None) => return false,
162        }
163        true
164    }
165}
166
167fn write_meta(meta: &gix::config::file::Metadata, out: &mut impl std::io::Write) -> std::io::Result<()> {
168    writeln!(
169        out,
170        "# From '{}' ({:?}{}{})",
171        meta.path
172            .as_deref()
173            .map_or_else(|| "memory".into(), |p| p.display().to_string()),
174        meta.source,
175        if meta.level != 0 {
176            format!(", include level {}", meta.level)
177        } else {
178            Default::default()
179        },
180        if meta.trust != gix::sec::Trust::Full {
181            ", untrusted"
182        } else {
183            Default::default()
184        }
185    )
186}