1use std::fmt::Display;
2use std::path::{Path, PathBuf};
3
4use eyre::Context;
5use tracing::instrument;
6
7use super::repo::wrap_git_error;
8
9pub struct Config {
11 inner: git2::Config,
12}
13
14impl From<git2::Config> for Config {
15 fn from(config: git2::Config) -> Self {
16 Config { inner: config }
17 }
18}
19
20impl std::fmt::Debug for Config {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(f, "<Git repository config>")
23 }
24}
25
26#[derive(Debug)]
27enum ConfigValueInner {
28 String(String),
29 Int(i32),
30 Bool(bool),
31}
32
33#[derive(Debug)]
35pub struct ConfigValue {
36 inner: ConfigValueInner,
37}
38
39impl From<bool> for ConfigValue {
40 fn from(value: bool) -> ConfigValue {
41 ConfigValue {
42 inner: ConfigValueInner::Bool(value),
43 }
44 }
45}
46
47impl From<i32> for ConfigValue {
48 fn from(value: i32) -> Self {
49 ConfigValue {
50 inner: ConfigValueInner::Int(value),
51 }
52 }
53}
54
55impl From<String> for ConfigValue {
56 fn from(value: String) -> ConfigValue {
57 ConfigValue {
58 inner: ConfigValueInner::String(value),
59 }
60 }
61}
62
63impl From<&str> for ConfigValue {
64 fn from(value: &str) -> ConfigValue {
65 ConfigValue {
66 inner: ConfigValueInner::String(value.to_string()),
67 }
68 }
69}
70
71impl Display for ConfigValue {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match &self.inner {
74 ConfigValueInner::String(value) => write!(f, "{value}"),
75 ConfigValueInner::Int(value) => write!(f, "{value}"),
76 ConfigValueInner::Bool(value) => write!(f, "{value:?}"),
77 }
78 }
79}
80
81pub trait GetConfigValue<V> {
83 fn get_from_config(config: &Config, key: impl AsRef<str>) -> eyre::Result<Option<V>>;
85}
86
87impl GetConfigValue<String> for String {
88 fn get_from_config(config: &Config, key: impl AsRef<str>) -> eyre::Result<Option<String>> {
89 #[instrument]
90 fn inner(config: &Config, key: &str) -> eyre::Result<Option<String>> {
91 let value = match config.inner.get_string(key) {
92 Ok(value) => Some(value),
93 Err(err) if err.code() == git2::ErrorCode::NotFound => None,
94 Err(err) => {
95 return Err(wrap_git_error(err))
96 .wrap_err("Looking up string value for config key");
97 }
98 };
99 Ok(value)
100 }
101 inner(config, key.as_ref())
102 }
103}
104
105impl GetConfigValue<bool> for bool {
106 fn get_from_config(config: &Config, key: impl AsRef<str>) -> eyre::Result<Option<bool>> {
107 #[instrument]
108 fn inner(config: &Config, key: &str) -> eyre::Result<Option<bool>> {
109 let value = match config.inner.get_bool(key) {
110 Ok(value) => Some(value),
111 Err(err) if err.code() == git2::ErrorCode::NotFound => None,
112 Err(err) => {
113 return Err(wrap_git_error(err))
114 .wrap_err("Looking up bool value for config key")
115 }
116 };
117 Ok(value)
118 }
119 inner(config, key.as_ref())
120 }
121}
122
123impl GetConfigValue<i32> for i32 {
124 fn get_from_config(config: &Config, key: impl AsRef<str>) -> eyre::Result<Option<i32>> {
125 #[instrument]
126 fn inner(config: &Config, key: &str) -> eyre::Result<Option<i32>> {
127 let value = match config.inner.get_i32(key) {
128 Ok(value) => Some(value),
129 Err(err) if err.code() == git2::ErrorCode::NotFound => None,
130 Err(err) => {
131 return Err(wrap_git_error(err))
132 .wrap_err("Looking up bool value for config key")
133 }
134 };
135 Ok(value)
136 }
137 inner(config, key.as_ref())
138 }
139}
140
141impl GetConfigValue<PathBuf> for PathBuf {
142 fn get_from_config(config: &Config, key: impl AsRef<str>) -> eyre::Result<Option<PathBuf>> {
143 #[instrument]
144 fn inner(config: &Config, key: &str) -> eyre::Result<Option<PathBuf>> {
145 let value = match config.inner.get_path(key.as_ref()) {
146 Ok(value) => Some(value),
147 Err(err) if err.code() == git2::ErrorCode::NotFound => None,
148 Err(err) => {
149 return Err(wrap_git_error(err))
150 .wrap_err("Looking up path value for config key")
151 }
152 };
153 Ok(value)
154 }
155 inner(config, key.as_ref())
156 }
157}
158
159pub trait ConfigRead {
161 fn into_config(self) -> Config;
165
166 fn list<S: AsRef<str>>(&self, glob_pattern: S) -> eyre::Result<Vec<(String, String)>>;
168
169 fn get<V: GetConfigValue<V>, S: AsRef<str>>(&self, key: S) -> eyre::Result<Option<V>>;
171
172 fn get_or<V: GetConfigValue<V>, S: AsRef<str>>(&self, key: S, default: V) -> eyre::Result<V> {
174 let result = self.get(key)?;
175 Ok(result.unwrap_or(default))
176 }
177
178 fn get_or_else<V: GetConfigValue<V>, S: AsRef<str>, F: FnOnce() -> V>(
180 &self,
181 key: S,
182 default: F,
183 ) -> eyre::Result<V> {
184 let result = self.get(key)?;
185 match result {
186 Some(result) => Ok(result),
187 None => Ok(default()),
188 }
189 }
190}
191
192impl ConfigRead for Config {
193 fn into_config(self) -> Self {
194 self
195 }
196
197 fn get<V: GetConfigValue<V>, S: AsRef<str>>(&self, key: S) -> eyre::Result<Option<V>> {
199 V::get_from_config(self, key)
200 }
201
202 fn list<S: AsRef<str>>(&self, glob_pattern: S) -> eyre::Result<Vec<(String, String)>> {
203 let glob_pattern = glob_pattern.as_ref();
204 let entries = self.inner.entries(Some(glob_pattern)).wrap_err_with(|| {
205 format!("Reading config entries for glob pattern {glob_pattern:?}")
206 })?;
207 let mut result = Vec::new();
208 entries
209 .for_each(|entry| {
210 if let (Some(name), Some(value)) = (entry.name(), entry.value()) {
211 result.push((name.to_owned(), value.to_owned()));
212 }
213 })
214 .wrap_err_with(|| {
215 format!("Iterating config entries for glob pattern {glob_pattern:?}")
216 })?;
217 Ok(result)
218 }
219}
220
221pub trait ConfigWrite {
223 fn set(&mut self, key: impl AsRef<str>, value: impl Into<ConfigValue>) -> eyre::Result<()>;
225
226 fn remove(&mut self, key: impl AsRef<str>) -> eyre::Result<()>;
228
229 fn set_multivar(
233 &mut self,
234 key: impl AsRef<str>,
235 regex: impl AsRef<str>,
236 value: impl AsRef<str>,
237 ) -> eyre::Result<()>;
238
239 fn remove_multivar(&mut self, key: impl AsRef<str>, regex: impl AsRef<str>)
242 -> eyre::Result<()>;
243}
244
245impl Config {
246 #[instrument]
251 pub fn open(path: &Path) -> eyre::Result<Self> {
252 let inner = git2::Config::open(path).map_err(wrap_git_error)?;
253 Ok(Config { inner })
254 }
255
256 #[instrument]
259 pub fn open_default() -> eyre::Result<Self> {
260 let inner = git2::Config::open_default().map_err(wrap_git_error)?;
261 Ok(Config { inner })
262 }
263
264 #[instrument]
265 fn set_inner(&mut self, key: &str, value: ConfigValue) -> eyre::Result<()> {
266 match &value.inner {
267 ConfigValueInner::String(value) => {
268 self.inner.set_str(key, value).map_err(wrap_git_error)
269 }
270 ConfigValueInner::Int(value) => self.inner.set_i32(key, *value).map_err(wrap_git_error),
271 ConfigValueInner::Bool(value) => {
272 self.inner.set_bool(key, *value).map_err(wrap_git_error)
273 }
274 }
275 }
276
277 #[instrument]
278 fn remove_inner(&mut self, key: &str) -> eyre::Result<()> {
279 self.inner
280 .remove(key)
281 .map_err(wrap_git_error)
282 .wrap_err("Removing config key")?;
283 Ok(())
284 }
285
286 #[instrument]
287 fn set_multivar_inner(&mut self, key: &str, regex: &str, value: &str) -> eyre::Result<()> {
288 self.inner
289 .set_multivar(key, regex, value)
290 .map_err(wrap_git_error)
291 }
292
293 #[instrument]
294 fn remove_multivar_inner(&mut self, key: &str, regex: &str) -> eyre::Result<()> {
295 let result = self.inner.remove_multivar(key, regex);
296 let result = match result {
297 Err(err) if err.code() == git2::ErrorCode::NotFound => {
298 Ok(())
300 }
301 result => result,
302 };
303 result.map_err(wrap_git_error)?;
304 Ok(())
305 }
306}
307
308impl ConfigWrite for Config {
309 fn set(&mut self, key: impl AsRef<str>, value: impl Into<ConfigValue>) -> eyre::Result<()> {
310 self.set_inner(key.as_ref(), value.into())
311 }
312
313 fn remove(&mut self, key: impl AsRef<str>) -> eyre::Result<()> {
314 self.remove_inner(key.as_ref())
315 }
316
317 fn set_multivar(
318 &mut self,
319 key: impl AsRef<str>,
320 regex: impl AsRef<str>,
321 value: impl AsRef<str>,
322 ) -> eyre::Result<()> {
323 self.set_multivar_inner(key.as_ref(), regex.as_ref(), value.as_ref())
324 }
325
326 fn remove_multivar(
327 &mut self,
328 key: impl AsRef<str>,
329 regex: impl AsRef<str>,
330 ) -> eyre::Result<()> {
331 self.remove_multivar_inner(key.as_ref(), regex.as_ref())
332 }
333}