git_x/
fixup.rs

1use std::process::Command;
2
3pub fn run(commit_hash: String, rebase: bool) {
4    // Validate the commit hash exists
5    if let Err(msg) = validate_commit_hash(&commit_hash) {
6        eprintln!("{}", format_error_message(msg));
7        return;
8    }
9
10    // Get current staged and unstaged changes
11    let has_changes = match check_for_changes() {
12        Ok(has_changes) => has_changes,
13        Err(msg) => {
14            eprintln!("{}", format_error_message(msg));
15            return;
16        }
17    };
18
19    if !has_changes {
20        eprintln!("{}", format_no_changes_message());
21        return;
22    }
23
24    // Get the short commit hash for better UX
25    let short_hash = match get_short_commit_hash(&commit_hash) {
26        Ok(hash) => hash,
27        Err(msg) => {
28            eprintln!("{}", format_error_message(msg));
29            return;
30        }
31    };
32
33    println!("{}", format_creating_fixup_message(&short_hash));
34
35    // Create the fixup commit
36    if let Err(msg) = create_fixup_commit(&commit_hash) {
37        eprintln!("{}", format_error_message(msg));
38        return;
39    }
40
41    println!("{}", format_fixup_created_message(&short_hash));
42
43    // Optionally run interactive rebase with autosquash
44    if rebase {
45        println!("{}", format_starting_rebase_message());
46        if let Err(msg) = run_autosquash_rebase(&commit_hash) {
47            eprintln!("{}", format_error_message(msg));
48            eprintln!("{}", format_manual_rebase_hint(&commit_hash));
49            return;
50        }
51        println!("{}", format_rebase_success_message());
52    } else {
53        println!("{}", format_manual_rebase_hint(&commit_hash));
54    }
55}
56
57// Helper function to validate commit hash exists
58fn validate_commit_hash(commit_hash: &str) -> Result<(), &'static str> {
59    let output = Command::new("git")
60        .args([
61            "rev-parse",
62            "--verify",
63            &format!("{commit_hash}^{{commit}}"),
64        ])
65        .output()
66        .map_err(|_| "Failed to validate commit hash")?;
67
68    if !output.status.success() {
69        return Err("Commit hash does not exist");
70    }
71
72    Ok(())
73}
74
75// Helper function to check for changes to commit
76fn check_for_changes() -> Result<bool, &'static str> {
77    let output = Command::new("git")
78        .args(["diff", "--cached", "--quiet"])
79        .status()
80        .map_err(|_| "Failed to check for staged changes")?;
81
82    // If staged changes exist, we're good
83    if !output.success() {
84        return Ok(true);
85    }
86
87    // Check for unstaged changes
88    let output = Command::new("git")
89        .args(["diff", "--quiet"])
90        .status()
91        .map_err(|_| "Failed to check for unstaged changes")?;
92
93    // If unstaged changes exist, we need to stage them
94    if !output.success() {
95        return Err("You have unstaged changes. Please stage them first with 'git add'");
96    }
97
98    Ok(false)
99}
100
101// Helper function to get short commit hash
102fn get_short_commit_hash(commit_hash: &str) -> Result<String, &'static str> {
103    let output = Command::new("git")
104        .args(["rev-parse", "--short", commit_hash])
105        .output()
106        .map_err(|_| "Failed to get short commit hash")?;
107
108    if !output.status.success() {
109        return Err("Failed to resolve commit hash");
110    }
111
112    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
113}
114
115// Helper function to create fixup commit
116fn create_fixup_commit(commit_hash: &str) -> Result<(), &'static str> {
117    let status = Command::new("git")
118        .args(["commit", &format!("--fixup={commit_hash}")])
119        .status()
120        .map_err(|_| "Failed to create fixup commit")?;
121
122    if !status.success() {
123        return Err("Failed to create fixup commit");
124    }
125
126    Ok(())
127}
128
129// Helper function to run autosquash rebase
130fn run_autosquash_rebase(commit_hash: &str) -> Result<(), &'static str> {
131    // Find the parent of the target commit for rebase
132    let output = Command::new("git")
133        .args(["rev-parse", &format!("{commit_hash}^")])
134        .output()
135        .map_err(|_| "Failed to find parent commit")?;
136
137    if !output.status.success() {
138        return Err("Cannot rebase - commit has no parent");
139    }
140
141    let parent_hash_string = String::from_utf8_lossy(&output.stdout);
142    let parent_hash = parent_hash_string.trim();
143
144    let status = Command::new("git")
145        .args(["rebase", "-i", "--autosquash", parent_hash])
146        .status()
147        .map_err(|_| "Failed to start interactive rebase")?;
148
149    if !status.success() {
150        return Err("Interactive rebase failed");
151    }
152
153    Ok(())
154}
155
156// Helper function to get git commit args for fixup
157pub fn get_git_fixup_args() -> [&'static str; 2] {
158    ["commit", "--fixup"]
159}
160
161// Helper function to get git rebase args
162pub fn get_git_rebase_args() -> [&'static str; 3] {
163    ["rebase", "-i", "--autosquash"]
164}
165
166// Helper function to format error message
167pub fn format_error_message(msg: &str) -> String {
168    format!("❌ {msg}")
169}
170
171// Helper function to format no changes message
172pub fn format_no_changes_message() -> &'static str {
173    "❌ No staged changes found. Please stage your changes first with 'git add'"
174}
175
176// Helper function to format creating fixup message
177pub fn format_creating_fixup_message(short_hash: &str) -> String {
178    format!("🔧 Creating fixup commit for {short_hash}...")
179}
180
181// Helper function to format fixup created message
182pub fn format_fixup_created_message(short_hash: &str) -> String {
183    format!("✅ Fixup commit created for {short_hash}")
184}
185
186// Helper function to format starting rebase message
187pub fn format_starting_rebase_message() -> &'static str {
188    "🔄 Starting interactive rebase with autosquash..."
189}
190
191// Helper function to format rebase success message
192pub fn format_rebase_success_message() -> &'static str {
193    "✅ Interactive rebase completed successfully"
194}
195
196// Helper function to format manual rebase hint
197pub fn format_manual_rebase_hint(commit_hash: &str) -> String {
198    format!("💡 To squash the fixup commit, run: git rebase -i --autosquash {commit_hash}^")
199}
200
201// Helper function to check if commit hash is valid format
202pub fn is_valid_commit_hash_format(hash: &str) -> bool {
203    if hash.is_empty() {
204        return false;
205    }
206
207    // Must be 4-40 characters long (short to full hash)
208    if hash.len() < 4 || hash.len() > 40 {
209        return false;
210    }
211
212    // Must contain only hex characters
213    hash.chars().all(|c| c.is_ascii_hexdigit())
214}
215
216// Helper function to format commit validation rules
217pub fn get_commit_hash_validation_rules() -> &'static [&'static str] {
218    &[
219        "Must be 4-40 characters long",
220        "Must contain only hex characters (0-9, a-f)",
221        "Must reference an existing commit",
222    ]
223}