1use std::{fmt, sync::Arc};
2
3use parking_lot::Mutex;
4use rayon::prelude::*;
5
6use crate::{
7 analysis::extract_scope_candidates,
8 api::{generate_conventional_analysis, generate_summary_from_analysis},
9 config::CommitConfig,
10 diff::smart_truncate_diff,
11 error::{CommitGenError, Result},
12 git::{
13 check_working_tree_clean, create_backup_branch, get_commit_list, get_commit_metadata,
14 get_git_diff, get_git_stat, rewrite_history,
15 },
16 normalization::{format_commit_message, post_process_commit_message},
17 types::{Args, CommitMetadata, ConventionalCommit, Mode},
18 validation::validate_commit_message,
19};
20
21pub fn run_rewrite_mode(args: &Args, config: &CommitConfig) -> Result<()> {
23 if !args.rewrite_dry_run
25 && args.rewrite_preview.is_none()
26 && !check_working_tree_clean(&args.dir)?
27 {
28 return Err(CommitGenError::Other(
29 "Working directory not clean. Commit or stash changes first.".to_string(),
30 ));
31 }
32
33 println!("š Collecting commits...");
35 let mut commit_hashes = get_commit_list(args.rewrite_start.as_deref(), &args.dir)?;
36
37 if let Some(n) = args.rewrite_preview {
38 commit_hashes.truncate(n);
39 }
40
41 println!("Found {} commits to process", commit_hashes.len());
42
43 println!("š Extracting commit metadata...");
45 let commits: Vec<CommitMetadata> = commit_hashes
46 .iter()
47 .enumerate()
48 .map(|(i, hash)| {
49 if (i + 1) % 50 == 0 {
50 eprintln!(" {}/{}...", i + 1, commit_hashes.len());
51 }
52 get_commit_metadata(hash, &args.dir)
53 })
54 .collect::<Result<Vec<_>>>()?;
55
56 if args.rewrite_dry_run && args.rewrite_preview.is_some() {
58 print_preview_list(&commits);
59 return Ok(());
60 }
61
62 println!("š¤ Converting to conventional commits (parallel={})...\n", args.rewrite_parallel);
64
65 let mut rewrite_config = config.clone();
67 rewrite_config.exclude_old_message = true;
68
69 let new_messages = generate_messages_parallel(&commits, &rewrite_config, args)?;
70
71 print_conversion_results(&commits, &new_messages);
73
74 if args.rewrite_dry_run {
76 println!("\n=== DRY RUN - No changes made ===");
77 println!("Run without --rewrite-dry-run to apply changes");
78 return Ok(());
79 }
80
81 if args.rewrite_preview.is_some() {
82 println!("\nRun without --rewrite-preview to rewrite all history");
83 return Ok(());
84 }
85
86 println!("\nš¾ Creating backup branch...");
88 let backup = create_backup_branch(&args.dir)?;
89 println!("ā Backup: {backup}");
90
91 println!("\nā ļø Rewriting history...");
93 rewrite_history(&commits, &new_messages, &args.dir)?;
94
95 println!("\nā
Done! Rewrote {} commits", commits.len());
96 println!("Restore with: git reset --hard {backup}");
97
98 Ok(())
99}
100
101fn generate_messages_parallel(
103 commits: &[CommitMetadata],
104 config: &CommitConfig,
105 args: &Args,
106) -> Result<Vec<String>> {
107 let new_messages = Arc::new(Mutex::new(vec![String::new(); commits.len()]));
108 let errors = Arc::new(Mutex::new(Vec::new()));
109
110 rayon::ThreadPoolBuilder::new()
111 .num_threads(args.rewrite_parallel)
112 .build()
113 .map_err(|e| CommitGenError::Other(format!("Failed to create thread pool: {e}")))?
114 .install(|| {
115 commits.par_iter().enumerate().for_each(|(idx, commit)| {
116 match generate_for_commit(commit, config, &args.dir) {
117 Ok(new_msg) => {
118 new_messages.lock()[idx].clone_from(&new_msg);
119
120 let old = commit.message.lines().next().unwrap_or("");
122 let new = new_msg.lines().next().unwrap_or("");
123
124 println!("[{:3}/{:3}] {}", idx + 1, commits.len(), &commit.hash[..8]);
125 println!(" - {}", TruncStr(old, 60));
126 println!(" + {}", TruncStr(new, 60));
127 println!();
128 },
129 Err(e) => {
130 eprintln!(
131 "[{:3}/{:3}] {} ā ERROR: {}",
132 idx + 1,
133 commits.len(),
134 &commit.hash[..8],
135 e
136 );
137 new_messages.lock()[idx].clone_from(&commit.message);
139 errors.lock().push((idx, e.to_string()));
140 },
141 }
142 });
143 });
144
145 let final_messages = Arc::try_unwrap(new_messages).unwrap().into_inner();
146 let error_list = Arc::try_unwrap(errors).unwrap().into_inner();
147
148 if !error_list.is_empty() {
149 eprintln!("\nā ļø {} commits failed, kept original messages", error_list.len());
150 }
151
152 Ok(final_messages)
153}
154
155fn generate_for_commit(
157 commit: &CommitMetadata,
158 config: &CommitConfig,
159 dir: &str,
160) -> Result<String> {
161 let diff = get_git_diff(&Mode::Commit, Some(&commit.hash), dir, config)?;
164 let stat = get_git_stat(&Mode::Commit, Some(&commit.hash), dir, config)?;
165
166 let diff = if diff.len() > config.max_diff_length {
168 smart_truncate_diff(&diff, config.max_diff_length, config)
169 } else {
170 diff
171 };
172
173 let (scope_candidates_str, _) =
175 extract_scope_candidates(&Mode::Commit, Some(&commit.hash), dir, config)?;
176
177 let analysis = generate_conventional_analysis(
179 &stat,
180 &diff,
181 &config.analysis_model,
182 None, &scope_candidates_str,
184 config,
185 )?;
186
187 let summary = generate_summary_from_analysis(
189 &stat,
190 analysis.commit_type.as_str(),
191 analysis.scope.as_ref().map(|s| s.as_str()),
192 &analysis.body,
193 None, config,
195 )?;
196
197 let mut commit_msg = ConventionalCommit {
199 commit_type: analysis.commit_type,
200 scope: analysis.scope,
201 summary,
202 body: analysis.body,
203 footers: analysis
204 .issue_refs
205 .into_iter()
206 .map(|r| format!("Refs {r}"))
207 .collect(),
208 };
209
210 post_process_commit_message(&mut commit_msg, config);
212 validate_commit_message(&commit_msg, config)?;
213
214 Ok(format_commit_message(&commit_msg))
216}
217
218fn print_preview_list(commits: &[CommitMetadata]) {
220 println!("\n=== PREVIEW - Showing {} commits (no API calls) ===\n", commits.len());
221
222 for (i, commit) in commits.iter().enumerate() {
223 let summary = commit
224 .message
225 .lines()
226 .next()
227 .unwrap_or("")
228 .chars()
229 .take(70)
230 .collect::<String>();
231
232 println!("[{:3}] {} - {}", i + 1, &commit.hash[..8], summary);
233 }
234
235 println!("\nRun without --rewrite-preview to regenerate commits");
236}
237
238fn print_conversion_results(commits: &[CommitMetadata], new_messages: &[String]) {
240 println!("\nā Processed {} commits\n", commits.len());
241
242 let show_count = 3.min(commits.len());
244 if show_count > 0 {
245 println!("=== Sample conversions ===\n");
246 for i in 0..show_count {
247 let old = commits[i].message.lines().next().unwrap_or("");
248 let new = new_messages[i].lines().next().unwrap_or("");
249
250 println!("[{}] {}", i + 1, &commits[i].hash[..8]);
251 println!(" - {}", TruncStr(old, 70));
252 println!(" + {}", TruncStr(new, 70));
253 println!();
254 }
255 }
256}
257
258struct TruncStr<'a>(&'a str, usize);
259
260impl fmt::Display for TruncStr<'_> {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 if self.0.len() <= self.1 {
263 f.write_str(self.0)
264 } else {
265 let n = self.0.floor_char_boundary(self.1);
266 f.write_str(&self.0[..n])?;
267 f.write_str("...")
268 }
269 }
270}