git_prole/git/
config.rs

1use std::fmt::Debug;
2
3use command_error::CommandExt;
4use command_error::OutputContext;
5use miette::miette;
6use tracing::instrument;
7use utf8_command::Utf8Output;
8
9use super::GitLike;
10
11/// Git methods for dealing with config.
12#[repr(transparent)]
13pub struct GitConfig<'a, G>(&'a G);
14
15impl<G> Debug for GitConfig<'_, G>
16where
17    G: GitLike,
18{
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        f.debug_tuple("GitConfig")
21            .field(&self.0.get_current_dir().as_ref())
22            .finish()
23    }
24}
25
26impl<'a, G> GitConfig<'a, G>
27where
28    G: GitLike,
29{
30    pub fn new(git: &'a G) -> Self {
31        Self(git)
32    }
33
34    /// Get a config setting by name and parse a value out of it.
35    pub fn get_and<R>(
36        &self,
37        key: &str,
38        parser: impl Fn(OutputContext<Utf8Output>, Option<String>) -> Result<R, command_error::Error>,
39    ) -> miette::Result<R> {
40        Ok(self
41            .0
42            .command()
43            .args(["config", "get", "--null", key])
44            .output_checked_as(|context: OutputContext<Utf8Output>| {
45                if context.status().success() {
46                    // TODO: Should this be a winnow parser?
47                    match context.output().stdout.as_str().split_once('\0') {
48                        Some((value, rest)) => {
49                            if !rest.is_empty() {
50                                tracing::warn!(
51                                    %key,
52                                    data=rest,
53                                    "Trailing data in `git config` output"
54                                );
55                            }
56                            let value = value.to_owned();
57                            parser(context, Some(value))
58                        }
59                        None => Err(context.error_msg("Output didn't contain any null bytes")),
60                    }
61                } else if let Some(1) = context.status().code() {
62                    parser(context, None)
63                } else {
64                    Err(context.error())
65                }
66            })?)
67    }
68
69    /// Get a config setting by name.
70    #[instrument(level = "trace")]
71    pub fn get(&self, key: &str) -> miette::Result<Option<String>> {
72        self.get_and(key, |_, value| Ok(value))
73    }
74
75    /// Check if this repository is bare.
76    #[instrument(level = "trace")]
77    pub fn is_bare(&self) -> miette::Result<bool> {
78        self.get_and("core.bare", |context, value| {
79            match value {
80                None => {
81                    // This seems to not happen in practice, but whatever.
82                    Ok(false)
83                }
84                Some(value) => match value.as_str() {
85                    "true" => Ok(true),
86                    "false" => Ok(false),
87                    _ => Err(context.error_msg(miette!(
88                        "Unexpected Git config value for `core.bare`: {value}"
89                    ))),
90                },
91            }
92        })
93    }
94
95    /// Set a local config setting.
96    #[instrument(level = "trace")]
97    pub fn set(&self, key: &str, value: &str) -> miette::Result<()> {
98        self.0
99            .command()
100            .args(["config", "set", key, value])
101            .output_checked_utf8()?;
102        Ok(())
103    }
104}