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    #[must_use]
145    pub fn scope(mut self, s: ConfigScope) -> Self {
146        self.scope = Some(s);
147        self
148    }
149}
150
151#[async_trait]
152impl GitCommand for ConfigCommand {
153    type Output = CommandOutput;
154
155    fn get_executor(&self) -> &CommandExecutor {
156        &self.executor
157    }
158
159    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
160        &mut self.executor
161    }
162
163    fn build_command_args(&self) -> Vec<String> {
164        let mut args = vec!["config".to_string()];
165        match self.scope {
166            Some(ConfigScope::Local) => args.push("--local".into()),
167            Some(ConfigScope::Global) => args.push("--global".into()),
168            Some(ConfigScope::System) => args.push("--system".into()),
169            Some(ConfigScope::Worktree) => args.push("--worktree".into()),
170            None => {}
171        }
172        match &self.action {
173            ConfigAction::Get { key } => args.push(key.clone()),
174            ConfigAction::GetAll { key } => {
175                args.push("--get-all".into());
176                args.push(key.clone());
177            }
178            ConfigAction::Set { key, value } => {
179                args.push(key.clone());
180                args.push(value.clone());
181            }
182            ConfigAction::Unset { key } => {
183                args.push("--unset".into());
184                args.push(key.clone());
185            }
186            ConfigAction::UnsetAll { key } => {
187                args.push("--unset-all".into());
188                args.push(key.clone());
189            }
190            ConfigAction::Add { key, value } => {
191                args.push("--add".into());
192                args.push(key.clone());
193                args.push(value.clone());
194            }
195            ConfigAction::List => args.push("--list".into()),
196        }
197        args
198    }
199
200    async fn execute(&self) -> Result<CommandOutput> {
201        // `git config --get` returns exit 1 when the key is missing; surface
202        // that as CommandFailed per our standard model.
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().to_string())
216            }
217            _ => Err(Error::invalid_config(
218                "execute_value only applies to get / get-all actions",
219            )),
220        }
221    }
222}