feat(integrations): add merge-method selection and provider-specific payloads

Introduce dedicated merge handling for integration review merges:

- Add src-tauri/src/integrations/merge.rs: implements merge_options (read provider repo settings), merge_payload (build provider-specific merge body) and a Tauri command get_integration_review_merge_options. Includes unit tests for behavior.
- Wire merge module into integrations.rs and pass an optional merge_method into provider-specific review action functions (GitHub, GitLab, Gitea, Azure DevOps). run_integration_review_action now accepts an optional merge_method, validates it early, and includes provider-specific merge payloads when performing a merge.
- Export the new command in src-tauri/src/main.rs so the frontend can request merge options.

Frontend changes to support selecting a merge method before merging:

- ConfirmDialog.svelte: add SelectMenu support and a select field to confirm requests.
- ReviewCenter.svelte: fetch integration merge options, show a merge-method selector in the merge confirmation, and pass the chosen method to the review action.
- Update types and git bindings to surface IntegrationMergeOptions / IntegrationMergeMethod and the getIntegrationReviewMergeOptions call (git.ts / types.ts changes staged).

Effect: users can pick a merge method appropriate to the provider/project; the integration layer generates the correct API payload per provider. Tests added for merge logic.
This commit is contained in:
2026-09-18 15:12:25 +02:00
parent ed484f5477
commit 6a40159f9f
7 changed files with 202 additions and 29 deletions
+22 -8
View File
@@ -1,3 +1,6 @@
mod merge;
pub use merge::get_integration_review_merge_options;
use merge::merge_payload;
mod issue_creation;
pub use issue_creation::*;
mod issue_actions;
@@ -864,13 +867,14 @@ fn github_review_action(
repository_name: &str,
number: u64,
action: &str,
merge_method: Option<&str>,
) -> Result<(), String> {
if repository_name.split('/').count() != 2 {
return Err("GitHub returned an invalid repository name.".to_string());
}
let endpoint = format!("{}/repos/{repository_name}/pulls/{number}", github_api_base_url(base_url)?);
let request = match action {
"merge" => client.put(format!("{endpoint}/merge")).json(&serde_json::json!({})),
"merge" => client.put(format!("{endpoint}/merge")).json(&merge_payload("github", merge_method)?),
"approve" => client.post(format!("{endpoint}/reviews")).json(&serde_json::json!({ "event": "APPROVE" })),
"close" => client.patch(&endpoint).json(&serde_json::json!({ "state": "closed" })),
"reopen" => client.patch(&endpoint).json(&serde_json::json!({ "state": "open" })),
@@ -893,13 +897,14 @@ fn gitlab_review_action(
repository_id: &str,
number: u64,
action: &str,
merge_method: Option<&str>,
) -> Result<(), String> {
if repository_id.trim().is_empty() {
return Err("GitLab returned an invalid project identifier.".to_string());
}
let endpoint = format!("{base_url}/api/v4/projects/{repository_id}/merge_requests/{number}");
let request = match action {
"merge" => client.put(format!("{endpoint}/merge")),
"merge" => client.put(format!("{endpoint}/merge")).json(&merge_payload("gitlab", merge_method)?),
"approve" => client.post(format!("{endpoint}/approve")),
"close" => client.put(&endpoint).query(&[("state_event", "close")]),
"reopen" => client.put(&endpoint).query(&[("state_event", "reopen")]),
@@ -921,13 +926,14 @@ fn gitea_review_action(
repository_name: &str,
number: u64,
action: &str,
merge_method: Option<&str>,
) -> Result<(), String> {
if repository_name.split('/').count() != 2 {
return Err("Gitea returned an invalid repository name.".to_string());
}
let endpoint = format!("{base_url}/api/v1/repos/{repository_name}/pulls/{number}");
let request = match action {
"merge" => client.post(format!("{endpoint}/merge")).json(&serde_json::json!({ "Do": "merge" })),
"merge" => client.post(format!("{endpoint}/merge")).json(&merge_payload("gitea", merge_method)?),
"approve" => client.post(format!("{endpoint}/reviews")).json(&serde_json::json!({ "event": "APPROVED", "body": "" })),
"close" => client.patch(&endpoint).json(&serde_json::json!({ "state": "closed" })),
"reopen" => client.patch(&endpoint).json(&serde_json::json!({ "state": "open" })),
@@ -978,6 +984,7 @@ fn azure_review_action(
repository_id: &str,
number: u64,
action: &str,
merge_method: Option<&str>,
) -> Result<(), String> {
let endpoint = azure_review_endpoint(base_url, repository_name, repository_id, number)?;
let auth_user = if username.trim().is_empty() { "gitty" } else { username };
@@ -1007,7 +1014,12 @@ fn azure_review_action(
let payload = current.json::<serde_json::Value>().map_err(|err| format!("Azure DevOps returned an unreadable pull request: {err}"))?;
let commit_id = value_string(&payload, &["lastMergeSourceCommit", "commitId"]);
if commit_id.is_empty() { return Err("Azure DevOps did not return the current source commit.".to_string()); }
serde_json::json!({ "status": "completed", "lastMergeSourceCommit": { "commitId": commit_id } })
{
let mut body = merge_payload("azure-devops", merge_method)?;
body["status"] = serde_json::json!("completed");
body["lastMergeSourceCommit"] = serde_json::json!({ "commitId": commit_id });
body
}
}
_ => return Err("Unsupported review action.".to_string()),
};
@@ -1184,19 +1196,21 @@ pub async fn run_integration_review_action(
repository_name: String,
number: u64,
action: String,
merge_method: Option<String>,
) -> Result<(), String> {
tokio::time::timeout(
REVIEW_REQUEST_TIMEOUT,
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()?;
match provider.as_str() {
"github" => github_review_action(&client, &base_url, &token, &repository_name, number, &action),
"gitlab" | "gitlab-self-hosted" => gitlab_review_action(&client, &base_url, &token, &repository_id, number, &action),
"gitea" => gitea_review_action(&client, &base_url, &token, &repository_name, number, &action),
"azure-devops" => azure_review_action(&client, &base_url, &username, &token, &repository_name, &repository_id, number, &action),
"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()),
}
}),