1
0

feat(git): Implement manual and bulk rewrite of git commit messages via LLM

This commit is contained in:
2026-07-12 11:23:21 +02:00
parent 39fdfb5cac
commit b73af714a5

View File

@@ -2880,6 +2880,275 @@ fn trigger_rewrite_now(
Ok(json!({ "success": true })) Ok(json!({ "success": true }))
} }
fn run_manual_rewrite_job(
app: AppHandle,
folder_path: String,
hashes: Vec<String>,
scope: &'static str,
) {
let state = app.state::<AppState>();
let total = hashes.len();
let (model, queued_before, original_birthday) = match state.store.lock() {
Ok(store) => {
let Some(folder) = store.folders.iter().find(|folder| folder.path == folder_path) else {
return;
};
(
store
.commit_model
.clone()
.unwrap_or_else(|| "qwen2.5-coder:7b".to_string()),
folder.llm_candidates.clone(),
folder.first_candidate_birthday,
)
}
Err(err) => {
eprintln!("[manualRewrite] Could not lock store: {err}");
return;
}
};
let mut identity_hashes = queued_before.clone();
identity_hashes.extend(hashes.clone());
let identities_before = identities_for_hashes(&folder_path, &identity_hashes);
let attempted_identities: HashMap<String, CommitIdentity> = hashes
.iter()
.filter_map(|hash| {
identities_before
.get(hash)
.cloned()
.map(|identity| (hash.clone(), identity))
})
.collect();
let queued_identities: HashSet<CommitIdentity> = queued_before
.iter()
.filter_map(|hash| resolve_commit_hash(&folder_path, hash))
.filter_map(|hash| identities_before.get(&hash).cloned())
.collect();
let mut successful_messages = HashMap::new();
let mut llm_failures: Vec<(String, String)> = Vec::new();
for (index, hash) in hashes.iter().enumerate() {
emit_rewrite_progress(
&app,
&folder_path,
scope,
"running",
index,
total,
Some(hash),
None,
);
match generate_llm_message_for_commit(&app, &folder_path, hash, &model) {
Ok(message) => {
successful_messages.insert(hash.clone(), message);
}
Err(err) => {
eprintln!("[manualRewrite] {} failed: {err}", short_hash(hash));
llm_failures.push((hash.clone(), err));
}
}
emit_rewrite_progress(
&app,
&folder_path,
scope,
"running",
index + 1,
total,
Some(hash),
None,
);
}
let successful_hashes: Vec<String> = hashes
.iter()
.filter(|hash| successful_messages.contains_key(*hash))
.cloned()
.collect();
let history_error = if successful_hashes.is_empty() {
None
} else {
match reword_commits_sequentially(&folder_path, &successful_messages, &successful_hashes) {
Ok(()) => None,
Err(err) => {
eprintln!("[manualRewrite] Git rewrite failed: {err}");
let _ = run_git(&folder_path, &["rebase", "--abort"]);
Some(err)
}
}
};
let mut desired_identities = queued_identities;
if history_error.is_none() {
for hash in &successful_hashes {
if let Some(identity) = attempted_identities.get(hash) {
desired_identities.remove(identity);
}
}
for (hash, _) in &llm_failures {
if let Some(identity) = attempted_identities.get(hash) {
desired_identities.insert(identity.clone());
}
}
} else {
desired_identities.extend(attempted_identities.values().cloned());
}
let identities_after = current_commit_identities(&folder_path);
let mut candidates: Vec<String> = desired_identities
.iter()
.filter_map(|identity| identities_after.get(identity).cloned())
.collect();
for queued in &queued_before {
if resolve_commit_hash(&folder_path, queued).is_none() {
candidates.push(queued.clone());
}
}
if scope == "pending" || history_error.is_some() {
candidates = pending_rewrite_hashes(&folder_path, &candidates);
}
candidates.sort();
candidates.dedup();
if let Ok(mut store) = state.store.lock() {
if let Some(folder) = store
.folders
.iter_mut()
.find(|folder| folder.path == folder_path)
{
candidates.extend(folder.llm_buffer.clone());
candidates.sort();
candidates.dedup();
folder.llm_candidates = candidates;
folder.llm_buffer.clear();
folder.rewrite_in_progress = false;
folder.rewrite_started_at = None;
folder.first_candidate_birthday = if folder.llm_candidates.is_empty() {
None
} else {
original_birthday.or_else(|| Some(now_ms()))
};
}
}
if let Err(err) = save_store(&state) {
eprintln!("[manualRewrite] Could not save rewrite state: {err}");
}
emit(&app, "repo-updated", folder_path.clone());
let errors: Vec<String> = llm_failures
.iter()
.map(|(hash, err)| format!("{}: {err}", short_hash(hash)))
.chain(history_error.iter().map(|err| format!("Git: {err}")))
.collect();
let status = if history_error.is_some() || successful_hashes.is_empty() {
"failed"
} else if llm_failures.is_empty() {
"succeeded"
} else {
"partial"
};
let error_text = if errors.is_empty() {
None
} else {
Some(errors.join("\n"))
};
emit_rewrite_progress(
&app,
&folder_path,
scope,
status,
total,
total,
None,
error_text.as_deref(),
);
}
#[tauri::command]
fn rewrite_commit(
app: AppHandle,
state: tauri::State<'_, AppState>,
folder_path: String,
hash: String,
) -> CommandResult<Value> {
let full_hash = resolve_commit_hash(&folder_path, &hash)
.ok_or_else(|| format!("Commit {hash} was not found."))?;
if !is_commit_on_current_history(&folder_path, &full_hash) {
return Ok(json!({
"success": false,
"error": "This commit is not in the current HEAD history. Jump to its branch/history first."
}));
}
if is_rebase_in_progress(&folder_path) {
return Ok(json!({ "success": false, "error": "A Git rebase is already in progress." }));
}
{
let mut store = state.store.lock().map_err(|e| e.to_string())?;
let Some(folder) = store
.folders
.iter_mut()
.find(|folder| folder.path == folder_path)
else {
return Ok(json!({ "success": false, "error": "folder not found" }));
};
if folder.needs_relocation {
return Ok(json!({ "success": false, "error": "needs relocation" }));
}
if folder.rewrite_in_progress {
return Ok(json!({
"success": false,
"error": "Another rewrite is already running for this repository."
}));
}
folder.rewrite_in_progress = true;
folder.rewrite_started_at = Some(now_ms());
}
save_store(&state)?;
thread::spawn(move || run_manual_rewrite_job(app, folder_path, vec![full_hash], "single"));
Ok(json!({ "success": true, "count": 1 }))
}
#[tauri::command]
fn rewrite_pending_commits(
app: AppHandle,
state: tauri::State<'_, AppState>,
folder_path: String,
) -> CommandResult<Value> {
if is_rebase_in_progress(&folder_path) {
return Ok(json!({ "success": false, "error": "A Git rebase is already in progress." }));
}
let hashes = {
let mut store = state.store.lock().map_err(|e| e.to_string())?;
let Some(folder) = store
.folders
.iter_mut()
.find(|folder| folder.path == folder_path)
else {
return Ok(json!({ "success": false, "error": "folder not found" }));
};
if folder.needs_relocation {
return Ok(json!({ "success": false, "error": "needs relocation" }));
}
if folder.rewrite_in_progress {
return Ok(json!({
"success": false,
"error": "Another rewrite is already running for this repository."
}));
}
let hashes = pending_rewrite_hashes(&folder_path, &folder.llm_candidates);
if hashes.is_empty() {
return Ok(json!({ "success": false, "error": "There are no pending commits to rewrite." }));
}
folder.rewrite_in_progress = true;
folder.rewrite_started_at = Some(now_ms());
hashes
};
let count = hashes.len();
save_store(&state)?;
thread::spawn(move || run_manual_rewrite_job(app, folder_path, hashes, "pending"));
Ok(json!({ "success": true, "count": count }))
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct TreeContextInfo { struct TreeContextInfo {