1use crate::core::traits::*;
2use crate::core::{git::*, validation::Validate};
3use crate::{GitXError, Result};
4
5pub struct CommitCommands;
7
8impl CommitCommands {
9 pub fn fixup(commit_hash: &str, auto_rebase: bool) -> Result<String> {
11 FixupCommand::new(commit_hash.to_string(), auto_rebase).execute()
12 }
13
14 pub fn undo() -> Result<String> {
16 UndoCommand::new().execute()
17 }
18
19 pub fn bisect(action: BisectAction) -> Result<String> {
21 BisectCommand::new(action).execute()
22 }
23}
24
25pub struct FixupCommand {
27 commit_hash: String,
28 auto_rebase: bool,
29}
30
31impl FixupCommand {
32 pub fn new(commit_hash: String, auto_rebase: bool) -> Self {
33 Self {
34 commit_hash,
35 auto_rebase,
36 }
37 }
38
39 fn has_staged_changes() -> Result<bool> {
40 let staged = GitOperations::staged_files()?;
41 Ok(!staged.is_empty())
42 }
43}
44
45impl Command for FixupCommand {
46 fn execute(&self) -> Result<String> {
47 Validate::commit_hash(&self.commit_hash)?;
49
50 if !GitOperations::commit_exists(&self.commit_hash)? {
52 return Err(GitXError::GitCommand(format!(
53 "Commit '{}' does not exist",
54 self.commit_hash
55 )));
56 }
57
58 if !Self::has_staged_changes()? {
60 return Err(GitXError::GitCommand(
61 "No staged changes found. Please stage your changes first with 'git add'"
62 .to_string(),
63 ));
64 }
65
66 CommitOperations::fixup(&self.commit_hash)?;
68
69 let mut result = format!("ā
Fixup commit created for {}", self.commit_hash);
70
71 if self.auto_rebase {
72 match GitOperations::run_status(&[
74 "rebase",
75 "-i",
76 "--autosquash",
77 &format!("{}^", self.commit_hash),
78 ]) {
79 Ok(_) => {
80 result.push_str("\nā
Interactive rebase completed successfully");
81 }
82 Err(_) => {
83 result.push_str(&format!(
84 "\nš” To squash the fixup commit, run: git rebase -i --autosquash {}^",
85 self.commit_hash
86 ));
87 }
88 }
89 } else {
90 result.push_str(&format!(
91 "\nš” To squash the fixup commit, run: git rebase -i --autosquash {}^",
92 self.commit_hash
93 ));
94 }
95
96 Ok(result)
97 }
98
99 fn name(&self) -> &'static str {
100 "fixup"
101 }
102
103 fn description(&self) -> &'static str {
104 "Create fixup commits for easier interactive rebasing"
105 }
106}
107
108impl GitCommand for FixupCommand {}
109
110pub struct UndoCommand;
112
113impl Default for UndoCommand {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119impl UndoCommand {
120 pub fn new() -> Self {
121 Self
122 }
123}
124
125impl Command for UndoCommand {
126 fn execute(&self) -> Result<String> {
127 CommitOperations::undo_last()?;
128 Ok("ā
Undid last commit (soft reset)".to_string())
129 }
130
131 fn name(&self) -> &'static str {
132 "undo"
133 }
134
135 fn description(&self) -> &'static str {
136 "Undo the last commit with a soft reset"
137 }
138}
139
140impl GitCommand for UndoCommand {}
141impl Destructive for UndoCommand {
142 fn destruction_description(&self) -> String {
143 "This will undo your last commit (but keep the changes staged)".to_string()
144 }
145}
146
147#[derive(Debug, Clone)]
149pub enum BisectAction {
150 Start { bad: String, good: String },
151 Good,
152 Bad,
153 Skip,
154 Reset,
155 Status,
156}
157
158pub struct BisectCommand {
160 action: BisectAction,
161}
162
163impl BisectCommand {
164 pub fn new(action: BisectAction) -> Self {
165 Self { action }
166 }
167
168 fn is_bisecting() -> Result<bool> {
169 match GitOperations::repo_root() {
171 Ok(root) => {
172 let bisect_head = std::path::Path::new(&root).join(".git").join("BISECT_HEAD");
173 Ok(bisect_head.exists())
174 }
175 Err(_) => Ok(false),
176 }
177 }
178
179 fn execute_bisect_action(&self) -> Result<String> {
180 match &self.action {
181 BisectAction::Start { bad, good } => {
182 Validate::commit_hash(bad)?;
184 Validate::commit_hash(good)?;
185
186 if !GitOperations::commit_exists(bad)? {
187 return Err(GitXError::GitCommand(format!(
188 "Bad commit '{bad}' does not exist"
189 )));
190 }
191 if !GitOperations::commit_exists(good)? {
192 return Err(GitXError::GitCommand(format!(
193 "Good commit '{good}' does not exist"
194 )));
195 }
196
197 GitOperations::run_status(&["bisect", "start"])?;
198 GitOperations::run_status(&["bisect", "bad", bad])?;
199 GitOperations::run_status(&["bisect", "good", good])?;
200
201 Ok(format!(
202 "š Started bisect between {bad} (bad) and {good} (good)"
203 ))
204 }
205 BisectAction::Good => {
206 if !Self::is_bisecting()? {
207 return Err(GitXError::GitCommand("Not currently bisecting".to_string()));
208 }
209 GitOperations::run_status(&["bisect", "good"])?;
210 Ok("ā
Marked current commit as good".to_string())
211 }
212 BisectAction::Bad => {
213 if !Self::is_bisecting()? {
214 return Err(GitXError::GitCommand("Not currently bisecting".to_string()));
215 }
216 GitOperations::run_status(&["bisect", "bad"])?;
217 Ok("ā Marked current commit as bad".to_string())
218 }
219 BisectAction::Skip => {
220 if !Self::is_bisecting()? {
221 return Err(GitXError::GitCommand("Not currently bisecting".to_string()));
222 }
223 GitOperations::run_status(&["bisect", "skip"])?;
224 Ok("āļø Skipped current commit".to_string())
225 }
226 BisectAction::Reset => {
227 if !Self::is_bisecting()? {
228 return Err(GitXError::GitCommand("Not currently bisecting".to_string()));
229 }
230 GitOperations::run_status(&["bisect", "reset"])?;
231 Ok("š Reset bisect and returned to original branch".to_string())
232 }
233 BisectAction::Status => {
234 if !Self::is_bisecting()? {
235 return Ok("Not currently bisecting".to_string());
236 }
237
238 let log = GitOperations::run(&["bisect", "log"])
239 .unwrap_or_else(|_| "No bisect log available".to_string());
240 Ok(format!("š Bisect status:\n{log}"))
241 }
242 }
243 }
244}
245
246impl Command for BisectCommand {
247 fn execute(&self) -> Result<String> {
248 self.execute_bisect_action()
249 }
250
251 fn name(&self) -> &'static str {
252 "bisect"
253 }
254
255 fn description(&self) -> &'static str {
256 "Simplified Git bisect workflow for finding bugs"
257 }
258}
259
260impl GitCommand for BisectCommand {}
261
262impl Destructive for BisectCommand {
263 fn destruction_description(&self) -> String {
264 match &self.action {
265 BisectAction::Start { .. } => {
266 "This will start a bisect session and change your working directory".to_string()
267 }
268 BisectAction::Reset => {
269 "This will reset the bisect session and return to your original branch".to_string()
270 }
271 _ => "This will change your working directory to a different commit".to_string(),
272 }
273 }
274}