git_x/commands/
commit.rs

1use crate::core::traits::*;
2use crate::core::{git::*, validation::Validate};
3use crate::{GitXError, Result};
4
5/// Commit-related commands grouped together
6pub struct CommitCommands;
7
8impl CommitCommands {
9    /// Create a fixup commit
10    pub fn fixup(commit_hash: &str, auto_rebase: bool) -> Result<String> {
11        FixupCommand::new(commit_hash.to_string(), auto_rebase).execute()
12    }
13
14    /// Undo the last commit
15    pub fn undo() -> Result<String> {
16        UndoCommand::new().execute()
17    }
18
19    /// Bisect workflow
20    pub fn bisect(action: BisectAction) -> Result<String> {
21        BisectCommand::new(action).execute()
22    }
23}
24
25/// Command to create fixup commits
26pub 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 format
48        Validate::commit_hash(&self.commit_hash)?;
49
50        // Check if commit exists
51        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        // Check for staged changes
59        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        // Create fixup commit
67        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            // Perform interactive rebase with autosquash
73            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
110/// Command to undo the last commit
111pub 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/// Bisect workflow actions
148#[derive(Debug, Clone)]
149pub enum BisectAction {
150    Start { bad: String, good: String },
151    Good,
152    Bad,
153    Skip,
154    Reset,
155    Status,
156}
157
158/// Command for Git bisect workflow
159pub 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        // Check if .git/BISECT_HEAD exists
170        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 hashes
183                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}