feat(integrations): support automatic branch cleanup after merge

Add a new git::review_cleanup module that implements a CleanupPlan with
prepare() and finish() routines to safely remove/clean tracking and local
branches after a PR/MR is merged. The cleanup logic validates branch names,
ensures a clean worktree, checks remotes/URLs, verifies commits/ancestry,
protects against concurrent worktrees or divergent local/remote commits, and
performs authenticated fetch/push and ref updates. Unit tests for the cleanup
behavior are included.

Wire provider-side cleanup into integrations:
- add an integrations/cleanup module to read provider PR payloads and derive
  cleanup inputs
- run cleanup::prepare(...) before performing a merge when an optional
  cleanup_path is provided
- after a successful provider merge, run cleanup::finish(...); any failure is
  reported as MERGE_ACCEPTED_CLEANUP_FAILED

Also:
- export the new git review_cleanup module (src-tauri/src/git.rs)
- accept an optional cleanup_path parameter in run_integration_review_action
- remove the previous REVIEW_REQUEST_TIMEOUT wrapper around the spawned
  blocking task (the integration action is no longer wrapped with the 35s timeout)
This commit is contained in:
2026-09-18 15:22:57 +02:00
parent 6a40159f9f
commit 5db4f36abf
7 changed files with 791 additions and 18 deletions
+12 -6
View File
@@ -1,3 +1,4 @@
mod cleanup;
mod merge;
pub use merge::get_integration_review_merge_options;
use merge::merge_payload;
@@ -1197,26 +1198,31 @@ pub async fn run_integration_review_action(
number: u64,
action: String,
merge_method: Option<String>,
cleanup_path: Option<String>,
) -> Result<(), String> {
tokio::time::timeout(
REVIEW_REQUEST_TIMEOUT,
tauri::async_runtime::spawn_blocking(move || {
tauri::async_runtime::spawn_blocking(move || {
if token.trim().is_empty() { return Err("No token is stored for this integration.".to_string()); }
if !matches!(action.as_str(), "merge" | "approve" | "close" | "reopen") { return Err("Unsupported review action.".to_string()); }
if action == "merge" { merge_payload(&provider, merge_method.as_deref())?; }
let base_url = normalized_base_url(&base_url)?;
let client = client()?;
let cleanup = if action == "merge" {
cleanup_path.as_deref().map(|path| cleanup::prepare(&client, &base_url, &username, &token, &provider, &repository_id, &repository_name, number, path)).transpose()?
} else { None };
match provider.as_str() {
"github" => github_review_action(&client, &base_url, &token, &repository_name, number, &action, merge_method.as_deref()),
"gitlab" | "gitlab-self-hosted" => gitlab_review_action(&client, &base_url, &token, &repository_id, number, &action, merge_method.as_deref()),
"gitea" => gitea_review_action(&client, &base_url, &token, &repository_name, number, &action, merge_method.as_deref()),
"azure-devops" => azure_review_action(&client, &base_url, &username, &token, &repository_name, &repository_id, number, &action, merge_method.as_deref()),
_ => Err("Unsupported integration provider.".to_string()),
}?;
if let Some(cleanup) = cleanup {
cleanup::finish(&cleanup, &client, &base_url, &username, &token, &provider, &repository_id, &repository_name, number)
.map_err(|err| format!("MERGE_ACCEPTED_CLEANUP_FAILED: {err}"))?;
}
}),
)
Ok(())
})
.await
.map_err(|_| "The integration API did not respond within 35 seconds.".to_string())?
.map_err(|err| format!("Could not update review request: {err}"))?
}