gitoxide_core/repository/
config.rs1use anyhow::{Context, Result, bail};
2use gix::{bstr::BString, config::AsKey};
3use std::io::Write as _;
4
5use crate::OutputFormat;
6
7pub 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(§ion)) {
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
86pub 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}