Skip to main content

git_spawn/command/
config.rs

1//! `git config` — get and set repository or global options.
2
3use crate::command::{CommandExecutor, CommandOutput, GitCommand};
4use crate::error::{Error, Result};
5use async_trait::async_trait;
6
7/// Configuration scope.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ConfigScope {
10    /// `--local` (default for a repo).
11    Local,
12    /// `--global` (~/.gitconfig).
13    Global,
14    /// `--system` (system-wide).
15    System,
16    /// `--worktree`.
17    Worktree,
18}
19
20/// Actions supported by `git config`.
21#[derive(Debug, Clone)]
22pub enum ConfigAction {
23    /// Get a value.
24    Get {
25        /// Key, e.g. `"user.email"`.
26        key: String,
27    },
28    /// Get all values for a multi-valued key.
29    GetAll {
30        /// Key.
31        key: String,
32    },
33    /// Set a value.
34    Set {
35        /// Key.
36        key: String,
37        /// Value.
38        value: String,
39    },
40    /// Unset a value.
41    Unset {
42        /// Key.
43        key: String,
44    },
45    /// Unset all values for a key.
46    UnsetAll {
47        /// Key.
48        key: String,
49    },
50    /// Add an additional value for a multi-valued key.
51    Add {
52        /// Key.
53        key: String,
54        /// Value.
55        value: String,
56    },
57    /// List all config keys.
58    List,
59}
60
61/// Builder for `git config`.
62#[derive(Debug, Clone)]
63pub struct ConfigCommand {
64    /// Shared executor.
65    pub executor: CommandExecutor,
66    /// Action.
67    pub action: ConfigAction,
68    /// Optional scope.
69    pub scope: Option<ConfigScope>,
70}
71
72impl ConfigCommand {
73    /// `config <key>` — get a value.
74    pub fn get(key: impl Into<String>) -> Self {
75        Self {
76            executor: CommandExecutor::default(),
77            action: ConfigAction::Get { key: key.into() },
78            scope: None,
79        }
80    }
81
82    /// `config --get-all <key>`.
83    pub fn get_all(key: impl Into<String>) -> Self {
84        Self {
85            executor: CommandExecutor::default(),
86            action: ConfigAction::GetAll { key: key.into() },
87            scope: None,
88        }
89    }
90
91    /// `config <key> <value>` — set a value.
92    pub fn set(key: impl Into<String>, value: impl Into<String>) -> Self {
93        Self {
94            executor: CommandExecutor::default(),
95            action: ConfigAction::Set {
96                key: key.into(),
97                value: value.into(),
98            },
99            scope: None,
100        }
101    }
102
103    /// `config --unset <key>`.
104    pub fn unset(key: impl Into<String>) -> Self {
105        Self {
106            executor: CommandExecutor::default(),
107            action: ConfigAction::Unset { key: key.into() },
108            scope: None,
109        }
110    }
111
112    /// `config --unset-all <key>`.
113    pub fn unset_all(key: impl Into<String>) -> Self {
114        Self {
115            executor: CommandExecutor::default(),
116            action: ConfigAction::UnsetAll { key: key.into() },
117            scope: None,
118        }
119    }
120
121    /// `config --add <key> <value>`.
122    pub fn add(key: impl Into<String>, value: impl Into<String>) -> Self {
123        Self {
124            executor: CommandExecutor::default(),
125            action: ConfigAction::Add {
126                key: key.into(),
127                value: value.into(),
128            },
129            scope: None,
130        }
131    }
132
133    /// `config --list`.
134    #[must_use]
135    pub fn list() -> Self {
136        Self {
137            executor: CommandExecutor::default(),
138            action: ConfigAction::List,
139            scope: None,
140        }
141    }
142
143    /// Limit to a particular scope.
144    pub fn scope(&mut self, s: ConfigScope) -> &mut Self {
145        self.scope = Some(s);
146        self
147    }
148}
149
150#[async_trait]
151impl GitCommand for ConfigCommand {
152    type Output = CommandOutput;
153
154    fn get_executor(&self) -> &CommandExecutor {
155        &self.executor
156    }
157
158    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
159        &mut self.executor
160    }
161
162    fn build_command_args(&self) -> Vec<String> {
163        let mut args = vec!["config".to_string()];
164        match self.scope {
165            Some(ConfigScope::Local) => args.push("--local".into()),
166            Some(ConfigScope::Global) => args.push("--global".into()),
167            Some(ConfigScope::System) => args.push("--system".into()),
168            Some(ConfigScope::Worktree) => args.push("--worktree".into()),
169            None => {}
170        }
171        match &self.action {
172            ConfigAction::Get { key } => args.push(key.clone()),
173            ConfigAction::GetAll { key } => {
174                args.push("--get-all".into());
175                args.push(key.clone());
176            }
177            ConfigAction::Set { key, value } => {
178                args.push(key.clone());
179                args.push(value.clone());
180            }
181            ConfigAction::Unset { key } => {
182                args.push("--unset".into());
183                args.push(key.clone());
184            }
185            ConfigAction::UnsetAll { key } => {
186                args.push("--unset-all".into());
187                args.push(key.clone());
188            }
189            ConfigAction::Add { key, value } => {
190                args.push("--add".into());
191                args.push(key.clone());
192                args.push(value.clone());
193            }
194            ConfigAction::List => args.push("--list".into()),
195        }
196        args
197    }
198
199    async fn execute(&self) -> Result<CommandOutput> {
200        // `git config --get` returns exit 1 when the key is missing; surface
201        // that as CommandFailed per our standard model. Callers that want a
202        // missing key treated as `None` should use `execute_value_opt`.
203        self.execute_raw().await
204    }
205}
206
207impl ConfigCommand {
208    /// Convenience: run the command and return the trimmed value for `get`.
209    ///
210    /// Returns [`Error::InvalidConfig`] if the action isn't `get` or `get_all`.
211    pub async fn execute_value(&self) -> Result<String> {
212        match self.action {
213            ConfigAction::Get { .. } | ConfigAction::GetAll { .. } => {
214                let out = self.execute_raw().await?;
215                Ok(out.stdout_trimmed())
216            }
217            _ => Err(Error::invalid_config(
218                "execute_value only applies to get / get-all actions",
219            )),
220        }
221    }
222
223    /// Like [`execute_value`](Self::execute_value), but treats a missing key
224    /// (exit 1) as `Ok(None)` rather than [`Error::CommandFailed`].
225    ///
226    /// `git config --get` exits 1 when the key is absent -- a normal outcome,
227    /// not a failure. Genuine errors (exit >= 2) still return `Err`. Only valid
228    /// for `get` / `get-all` actions.
229    pub async fn execute_value_opt(&self) -> Result<Option<String>> {
230        match self.action {
231            ConfigAction::Get { .. } | ConfigAction::GetAll { .. } => {
232                match self.execute_raw().await {
233                    Ok(out) => Ok(Some(out.stdout_trimmed())),
234                    Err(Error::CommandFailed { exit_code: 1, .. }) => Ok(None),
235                    Err(e) => Err(e),
236                }
237            }
238            _ => Err(Error::invalid_config(
239                "execute_value_opt only applies to get / get-all actions",
240            )),
241        }
242    }
243}