git_x/commands/
commit.rs

1use crate::core::git::*;
2use crate::core::traits::*;
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        // Allow any Git reference (commit hash, branch, tag, etc.)
48        // Try to resolve reference to verify it exists
49        if GitOperations::run(&["rev-parse", "--verify", &self.commit_hash]).is_err() {
50            // Check if we're in a git repo
51            if GitOperations::repo_root().is_err() {
52                return Err(GitXError::GitCommand(
53                    "Commit hash does not exist".to_string(),
54                ));
55            } else {
56                return Err(GitXError::Parse(format!(
57                    "Invalid commit hash format: '{}'",
58                    self.commit_hash
59                )));
60            }
61        }
62
63        // Check for staged changes
64        if !Self::has_staged_changes()? {
65            return Err(GitXError::GitCommand(
66                "No staged changes found. Please stage your changes first with 'git add'"
67                    .to_string(),
68            ));
69        }
70
71        // Create fixup commit
72        CommitOperations::fixup(&self.commit_hash)?;
73
74        let mut result = format!("āœ… Fixup commit created for {}", self.commit_hash);
75
76        if self.auto_rebase {
77            result.push_str("\nšŸ”„ Starting interactive rebase with autosquash");
78            // Perform interactive rebase with autosquash
79            match GitOperations::run_status(&[
80                "rebase",
81                "-i",
82                "--autosquash",
83                &format!("{}^", self.commit_hash),
84            ]) {
85                Ok(_) => {
86                    result.push_str("\nāœ… Interactive rebase completed successfully");
87                }
88                Err(_) => {
89                    result.push_str(&format!(
90                        "\nšŸ’” To squash the fixup commit, run: git rebase -i --autosquash {}^",
91                        self.commit_hash
92                    ));
93                }
94            }
95        } else {
96            result.push_str(&format!(
97                "\nšŸ’” To squash the fixup commit, run: git rebase -i --autosquash {}^",
98                self.commit_hash
99            ));
100        }
101
102        Ok(result)
103    }
104
105    fn name(&self) -> &'static str {
106        "fixup"
107    }
108
109    fn description(&self) -> &'static str {
110        "Create fixup commits for easier interactive rebasing"
111    }
112}
113
114impl GitCommand for FixupCommand {}
115
116/// Command to undo the last commit
117pub struct UndoCommand;
118
119impl Default for UndoCommand {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl UndoCommand {
126    pub fn new() -> Self {
127        Self
128    }
129}
130
131impl Command for UndoCommand {
132    fn execute(&self) -> Result<String> {
133        GitOperations::run_status(&["reset", "--soft", "HEAD~1"])?;
134        Ok("āœ… Last commit undone (soft reset). Changes kept in working directory.".to_string())
135    }
136
137    fn name(&self) -> &'static str {
138        "undo"
139    }
140
141    fn description(&self) -> &'static str {
142        "Undo the last commit (without losing changes)"
143    }
144}
145
146impl GitCommand for UndoCommand {}
147impl Destructive for UndoCommand {
148    fn destruction_description(&self) -> String {
149        "This will undo your last commit (but keep the changes staged)".to_string()
150    }
151}
152
153/// Bisect workflow actions
154#[derive(Debug, Clone)]
155pub enum BisectAction {
156    Start { bad: String, good: String },
157    Good,
158    Bad,
159    Skip,
160    Reset,
161    Status,
162}
163
164/// Command for Git bisect workflow
165pub struct BisectCommand {
166    action: BisectAction,
167}
168
169impl BisectCommand {
170    pub fn new(action: BisectAction) -> Self {
171        Self { action }
172    }
173
174    fn is_bisecting() -> Result<bool> {
175        // Check if .git/BISECT_HEAD exists
176        match GitOperations::repo_root() {
177            Ok(root) => {
178                let bisect_head = std::path::Path::new(&root).join(".git").join("BISECT_HEAD");
179                Ok(bisect_head.exists())
180            }
181            Err(_) => Ok(false),
182        }
183    }
184
185    fn execute_bisect_action(&self) -> Result<String> {
186        match &self.action {
187            BisectAction::Start { bad, good } => {
188                // Allow any Git reference (commit hash, branch, tag, etc.)
189                // Don't validate as strict hex - Git will handle this
190
191                // Try to resolve references to verify they exist
192                if GitOperations::run(&["rev-parse", "--verify", bad]).is_err() {
193                    return Err(GitXError::GitCommand(format!(
194                        "Reference '{bad}' does not exist"
195                    )));
196                }
197                if GitOperations::run(&["rev-parse", "--verify", good]).is_err() {
198                    return Err(GitXError::GitCommand(format!(
199                        "Reference '{good}' does not exist"
200                    )));
201                }
202
203                // Start bisect and capture git output for proper feedback
204                let output = GitOperations::run(&["bisect", "start", bad, good])?;
205
206                let mut result =
207                    format!("šŸ” Starting bisect between {bad} (bad) and {good} (good)");
208                if !output.trim().is_empty() {
209                    result = format!("{}\n{}", output.trim(), result);
210                }
211                result.push_str("\nāœ… Checked out commit");
212
213                Ok(result)
214            }
215            BisectAction::Good => {
216                if !Self::is_bisecting()? {
217                    return Err(GitXError::GitCommand(
218                        "Not currently in bisect mode".to_string(),
219                    ));
220                }
221                GitOperations::run_status(&["bisect", "good"])?;
222                Ok("āœ… Marked current commit as good".to_string())
223            }
224            BisectAction::Bad => {
225                if !Self::is_bisecting()? {
226                    return Err(GitXError::GitCommand(
227                        "Not currently in bisect mode".to_string(),
228                    ));
229                }
230                GitOperations::run_status(&["bisect", "bad"])?;
231                Ok("āŒ Marked current commit as bad".to_string())
232            }
233            BisectAction::Skip => {
234                if !Self::is_bisecting()? {
235                    return Err(GitXError::GitCommand(
236                        "Not currently in bisect mode".to_string(),
237                    ));
238                }
239                GitOperations::run_status(&["bisect", "skip"])?;
240                Ok("ā­ļø Skipped current commit".to_string())
241            }
242            BisectAction::Reset => {
243                if !Self::is_bisecting()? {
244                    return Ok("Not currently in bisect mode".to_string());
245                }
246                GitOperations::run_status(&["bisect", "reset"])?;
247                Ok("šŸ”„ Reset bisect and returned to original branch".to_string())
248            }
249            BisectAction::Status => {
250                if !Self::is_bisecting()? {
251                    return Ok("Not currently in bisect mode".to_string());
252                }
253
254                let log = GitOperations::run(&["bisect", "log"])
255                    .unwrap_or_else(|_| "No bisect log available".to_string());
256                Ok(format!("šŸ” Bisect status:\n{log}"))
257            }
258        }
259    }
260}
261
262impl Command for BisectCommand {
263    fn execute(&self) -> Result<String> {
264        self.execute_bisect_action()
265    }
266
267    fn name(&self) -> &'static str {
268        "bisect"
269    }
270
271    fn description(&self) -> &'static str {
272        "Simplified Git bisect workflow for finding bugs"
273    }
274}
275
276impl GitCommand for BisectCommand {}
277
278impl Destructive for BisectCommand {
279    fn destruction_description(&self) -> String {
280        match &self.action {
281            BisectAction::Start { .. } => {
282                "This will start a bisect session and change your working directory".to_string()
283            }
284            BisectAction::Reset => {
285                "This will reset the bisect session and return to your original branch".to_string()
286            }
287            _ => "This will change your working directory to a different commit".to_string(),
288        }
289    }
290}