branchless/git/
config.rs

1use std::fmt::Display;
2use std::path::{Path, PathBuf};
3
4use eyre::Context;
5use tracing::instrument;
6
7use super::repo::wrap_git_error;
8
9/// Wrapper around the config values stored on disk for Git.
10pub 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/// A wrapper around a possible value that can be set for a config key.
34#[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
81/// Trait used to make `Config::get` able to return multiple types.
82pub trait GetConfigValue<V> {
83    /// Get the given type of value from the config object.
84    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
159/// Read-only interface to Git's configuration.
160pub trait ConfigRead {
161    /// Convert this object into an owned, writable version of the
162    /// configuration. You should only use this if you know that it's safe to
163    /// write to the underlying configuration file.
164    fn into_config(self) -> Config;
165
166    /// Get all config key-value pairs matching a certain glob pattern.
167    fn list<S: AsRef<str>>(&self, glob_pattern: S) -> eyre::Result<Vec<(String, String)>>;
168
169    /// Get a config key of one of various possible types.
170    fn get<V: GetConfigValue<V>, S: AsRef<str>>(&self, key: S) -> eyre::Result<Option<V>>;
171
172    /// Same as `get`, but uses a default value if the config key doesn't exist.
173    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    /// Same as `get`, but computes a default value if the config key doesn't exist.
179    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    /// Get a config key of one of various possible types.
198    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
221/// Write-only interface to Git's configuration.
222pub trait ConfigWrite {
223    /// Set the given config key to the given value.
224    fn set(&mut self, key: impl AsRef<str>, value: impl Into<ConfigValue>) -> eyre::Result<()>;
225
226    /// Remove the given key from the configuration.
227    fn remove(&mut self, key: impl AsRef<str>) -> eyre::Result<()>;
228
229    /// Add or set a multivariable entry with the given to the given value. If a
230    /// key-value pair whose value matches the provided regex already exists,
231    /// that entry is overwritten.
232    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    /// Remove the multivariable entry with the provided key and whose value
240    /// matches the provided regex. If such a key is not present, does nothing.
241    fn remove_multivar(&mut self, key: impl AsRef<str>, regex: impl AsRef<str>)
242        -> eyre::Result<()>;
243}
244
245impl Config {
246    /// Open a configuration instance backed by the provided file. Unlike a
247    /// configuration instance opened directly from the Git repository, this
248    /// instance won't have a chain of parent configuration files to fall back
249    /// to for entry lookup.
250    #[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    /// Open a configuration instance derived from the global, XDG and
257    /// system configuration files.
258    #[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                // Do nothing.
299                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}