Skip to main content

git_spawn/command/
commit.rs

1//! `git commit` — record changes to the repository.
2
3use crate::command::{CommandExecutor, CommandOutput, GitCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::path::PathBuf;
7
8/// Builder for `git commit`.
9#[derive(Debug, Clone, Default)]
10pub struct CommitCommand {
11    /// Shared executor.
12    pub executor: CommandExecutor,
13    /// `-m`.
14    pub message: Option<String>,
15    /// `-F` file.
16    pub message_file: Option<PathBuf>,
17    /// `--all` / `-a`.
18    pub all: bool,
19    /// `--amend`.
20    pub amend: bool,
21    /// `--no-edit`.
22    pub no_edit: bool,
23    /// `--allow-empty`.
24    pub allow_empty: bool,
25    /// `--allow-empty-message`.
26    pub allow_empty_message: bool,
27    /// `--signoff` / `-s`.
28    pub signoff: bool,
29    /// `--no-verify`.
30    pub no_verify: bool,
31    /// `--author`.
32    pub author: Option<String>,
33    /// `--date`.
34    pub date: Option<String>,
35    /// `--only` specific paths.
36    pub only_paths: Vec<String>,
37    /// `--quiet`.
38    pub quiet: bool,
39    /// `--verbose`.
40    pub verbose: bool,
41}
42
43impl CommitCommand {
44    /// Build a bare commit command.
45    #[must_use]
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Convenience: commit with a message.
51    pub fn with_message(msg: impl Into<String>) -> Self {
52        let mut c = Self::new();
53        c.message = Some(msg.into());
54        c
55    }
56
57    /// Set the commit message (`-m`).
58    pub fn message(&mut self, msg: impl Into<String>) -> &mut Self {
59        self.message = Some(msg.into());
60        self
61    }
62
63    /// Read the commit message from a file (`-F`).
64    pub fn message_file(&mut self, path: impl Into<PathBuf>) -> &mut Self {
65        self.message_file = Some(path.into());
66        self
67    }
68
69    /// Stage all tracked, modified files before committing (`-a`).
70    pub fn all(&mut self) -> &mut Self {
71        self.all = true;
72        self
73    }
74
75    /// Amend the previous commit.
76    pub fn amend(&mut self) -> &mut Self {
77        self.amend = true;
78        self
79    }
80
81    /// Reuse the last commit message without opening the editor.
82    pub fn no_edit(&mut self) -> &mut Self {
83        self.no_edit = true;
84        self
85    }
86
87    /// Allow an empty commit.
88    pub fn allow_empty(&mut self) -> &mut Self {
89        self.allow_empty = true;
90        self
91    }
92
93    /// Allow an empty commit message.
94    pub fn allow_empty_message(&mut self) -> &mut Self {
95        self.allow_empty_message = true;
96        self
97    }
98
99    /// Add `Signed-off-by:` line.
100    pub fn signoff(&mut self) -> &mut Self {
101        self.signoff = true;
102        self
103    }
104
105    /// Skip pre-commit and commit-msg hooks.
106    pub fn no_verify(&mut self) -> &mut Self {
107        self.no_verify = true;
108        self
109    }
110
111    /// Override the author (`--author="Name <email>"`).
112    pub fn author(&mut self, a: impl Into<String>) -> &mut Self {
113        self.author = Some(a.into());
114        self
115    }
116
117    /// Override the author date.
118    pub fn date(&mut self, d: impl Into<String>) -> &mut Self {
119        self.date = Some(d.into());
120        self
121    }
122
123    /// Commit only the given paths.
124    pub fn only(&mut self, path: impl Into<String>) -> &mut Self {
125        self.only_paths.push(path.into());
126        self
127    }
128
129    /// Suppress output.
130    pub fn quiet(&mut self) -> &mut Self {
131        self.quiet = true;
132        self
133    }
134
135    /// Verbose diff output in the commit message editor.
136    pub fn verbose(&mut self) -> &mut Self {
137        self.verbose = true;
138        self
139    }
140}
141
142#[async_trait]
143impl GitCommand for CommitCommand {
144    type Output = CommandOutput;
145
146    fn get_executor(&self) -> &CommandExecutor {
147        &self.executor
148    }
149
150    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
151        &mut self.executor
152    }
153
154    fn build_command_args(&self) -> Vec<String> {
155        let mut args = vec!["commit".to_string()];
156        if self.all {
157            args.push("--all".into());
158        }
159        if self.amend {
160            args.push("--amend".into());
161        }
162        if self.no_edit {
163            args.push("--no-edit".into());
164        }
165        if self.allow_empty {
166            args.push("--allow-empty".into());
167        }
168        if self.allow_empty_message {
169            args.push("--allow-empty-message".into());
170        }
171        if self.signoff {
172            args.push("--signoff".into());
173        }
174        if self.no_verify {
175            args.push("--no-verify".into());
176        }
177        if self.quiet {
178            args.push("--quiet".into());
179        }
180        if self.verbose {
181            args.push("--verbose".into());
182        }
183        if let Some(a) = &self.author {
184            args.push(format!("--author={a}"));
185        }
186        if let Some(d) = &self.date {
187            args.push(format!("--date={d}"));
188        }
189        if let Some(m) = &self.message {
190            args.push("-m".into());
191            args.push(m.clone());
192        }
193        if let Some(f) = &self.message_file {
194            args.push("-F".into());
195            args.push(f.display().to_string());
196        }
197        if !self.only_paths.is_empty() {
198            args.push("--only".into());
199            args.push("--".into());
200            args.extend(self.only_paths.iter().cloned());
201        }
202        args
203    }
204
205    async fn execute(&self) -> Result<CommandOutput> {
206        self.execute_raw().await
207    }
208}