git_x/
fixup.rs

1use crate::core::validation::Validate;
2use crate::{GitXError, Result};
3use std::process::Command;
4
5pub fn run(commit_hash: String, rebase: bool) -> Result<()> {
6    // Validate commit hash format first
7    Validate::commit_hash(&commit_hash)?;
8
9    // Validate the commit hash exists
10    validate_commit_hash(&commit_hash).map_err(|e| GitXError::GitCommand(e.to_string()))?;
11
12    // Get current staged and unstaged changes
13    let has_changes = check_for_changes().map_err(|e| GitXError::GitCommand(e.to_string()))?;
14
15    if !has_changes {
16        return Err(GitXError::GitCommand(
17            "No staged changes found. Please stage your changes first with 'git add'".to_string(),
18        ));
19    }
20
21    // Get the short commit hash for better UX
22    let short_hash =
23        get_short_commit_hash(&commit_hash).map_err(|e| GitXError::GitCommand(e.to_string()))?;
24
25    println!("{}", &short_hash);
26
27    // Create the fixup commit
28    create_fixup_commit(&commit_hash).map_err(|e| GitXError::GitCommand(e.to_string()))?;
29
30    let short_hash2 = &short_hash;
31    println!("{short_hash2}");
32
33    // Optionally run interactive rebase with autosquash
34    if rebase {
35        println!("🔄 Starting interactive rebase with autosquash...");
36        if let Err(msg) = run_autosquash_rebase(&commit_hash) {
37            let msg1 = &msg.to_string();
38            eprintln!("{msg1}");
39            let commit_hash1 = &commit_hash;
40            eprintln!("{commit_hash1}");
41            return Ok(()); // Don't fail the whole command if rebase fails
42        }
43        println!("✅ Interactive rebase completed successfully");
44    } else {
45        let commit_hash1 = &commit_hash;
46        println!("{commit_hash1}");
47    }
48    Ok(())
49}
50
51fn validate_commit_hash(commit_hash: &str) -> Result<()> {
52    let output = Command::new("git")
53        .args([
54            "rev-parse",
55            "--verify",
56            &format!("{commit_hash}^{{commit}}"),
57        ])
58        .output()
59        .map_err(GitXError::Io)?;
60
61    if !output.status.success() {
62        return Err(GitXError::GitCommand(
63            "Commit hash does not exist".to_string(),
64        ));
65    }
66
67    Ok(())
68}
69
70fn check_for_changes() -> Result<bool> {
71    let output = Command::new("git")
72        .args(["diff", "--cached", "--quiet"])
73        .status()
74        .map_err(GitXError::Io)?;
75
76    // If staged changes exist, we're good
77    if !output.success() {
78        return Ok(true);
79    }
80
81    // Check for unstaged changes
82    let output = Command::new("git")
83        .args(["diff", "--quiet"])
84        .status()
85        .map_err(GitXError::Io)?;
86
87    // If unstaged changes exist, we need to stage them
88    if !output.success() {
89        return Err(GitXError::GitCommand(
90            "You have unstaged changes. Please stage them first with 'git add'".to_string(),
91        ));
92    }
93
94    Ok(false)
95}
96
97fn get_short_commit_hash(commit_hash: &str) -> Result<String> {
98    let output = Command::new("git")
99        .args(["rev-parse", "--short", commit_hash])
100        .output()
101        .map_err(GitXError::Io)?;
102
103    if !output.status.success() {
104        return Err(GitXError::GitCommand(
105            "Failed to resolve commit hash".to_string(),
106        ));
107    }
108
109    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
110}
111
112fn create_fixup_commit(commit_hash: &str) -> Result<()> {
113    let status = Command::new("git")
114        .args(["commit", &format!("--fixup={commit_hash}")])
115        .status()
116        .map_err(GitXError::Io)?;
117
118    if !status.success() {
119        return Err(GitXError::GitCommand(
120            "Failed to create fixup commit".to_string(),
121        ));
122    }
123
124    Ok(())
125}
126
127fn run_autosquash_rebase(commit_hash: &str) -> Result<()> {
128    // Find the parent of the target commit for rebase
129    let output = Command::new("git")
130        .args(["rev-parse", &format!("{commit_hash}^")])
131        .output()
132        .map_err(GitXError::Io)?;
133
134    if !output.status.success() {
135        return Err(GitXError::GitCommand(
136            "Cannot rebase - commit has no parent".to_string(),
137        ));
138    }
139
140    let parent_hash_string = String::from_utf8_lossy(&output.stdout);
141    let parent_hash = parent_hash_string.trim();
142
143    let status = Command::new("git")
144        .args(["rebase", "-i", "--autosquash", parent_hash])
145        .status()
146        .map_err(GitXError::Io)?;
147
148    if !status.success() {
149        return Err(GitXError::GitCommand(
150            "Interactive rebase failed".to_string(),
151        ));
152    }
153
154    Ok(())
155}