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