use super::*; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReviewMergeOptions { methods: Vec, default_method: String, } fn merge_options(provider: &str, repository: &serde_json::Value) -> Result { let candidates: &[(&str, &str)] = match provider { "gitea" => &[("merge", "allow_merge_commits"), ("rebase", "allow_rebase"), ("rebase-merge", "allow_rebase_explicit"), ("squash", "allow_squash_merge"), ("fast-forward-only", "allow_fast_forward_only_merge")], "github" => &[("merge", "allow_merge_commit"), ("squash", "allow_squash_merge"), ("rebase", "allow_rebase_merge")], "azure-devops" => &[("merge", ""), ("squash", ""), ("rebase", ""), ("rebase-merge", "")], "gitlab" | "gitlab-self-hosted" => &[], _ => return Err("Unsupported integration provider.".into()), }; let mut methods: Vec = candidates.iter() .filter(|(_, field)| field.is_empty() || repository.get(*field).and_then(serde_json::Value::as_bool) == Some(true)) .map(|(method, _)| method.to_string()).collect(); let preferred = if provider.starts_with("gitlab") { match repository.get("squash_option").and_then(serde_json::Value::as_str) { Some("always") => { methods.push("squash".into()); "squash" }, Some("never") => { methods.push("merge".into()); "merge" }, Some("default_on") => { methods.extend(["merge".into(), "squash".into()]); "squash" }, Some("default_off") => { methods.extend(["merge".into(), "squash".into()]); "merge" }, // Older servers may not expose squash settings; leave the server's default intact. _ => { methods.push("default".into()); "default" }, } } else { repository.get("default_merge_style").and_then(serde_json::Value::as_str).unwrap_or("merge") }; let default_method = methods.iter().find(|method| method.as_str() == preferred) .or_else(|| methods.first()).cloned().unwrap_or_default(); Ok(ReviewMergeOptions { methods, default_method }) } pub(super) fn merge_payload(provider: &str, method: Option<&str>) -> Result { let method = method.unwrap_or("default"); match (provider, method) { ("github", "default") | ("gitlab" | "gitlab-self-hosted" | "azure-devops", "default") => Ok(serde_json::json!({})), ("github", "merge" | "squash" | "rebase") => Ok(serde_json::json!({ "merge_method": method })), ("gitea", "default") => Ok(serde_json::json!({ "Do": "merge" })), ("gitea", "merge" | "squash" | "rebase" | "rebase-merge" | "fast-forward-only") => Ok(serde_json::json!({ "Do": method })), ("gitlab" | "gitlab-self-hosted", "merge" | "squash") => Ok(serde_json::json!({ "squash": method == "squash" })), ("azure-devops", "merge" | "squash" | "rebase" | "rebase-merge") => { let strategy = match method { "merge" => "noFastForward", "rebase-merge" => "rebaseMerge", other => other }; Ok(serde_json::json!({ "completionOptions": { "mergeStrategy": strategy } })) }, _ => Err("Unsupported merge method for this integration provider.".into()), } } #[tauri::command] pub async fn get_integration_review_merge_options(provider: String, base_url: String, token: String, repository_id: String, repository_name: String) -> Result { 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.".into()); } let base = normalized_base_url(&base_url)?; if provider == "azure-devops" { return merge_options(&provider, &serde_json::json!({})); } let client = client()?; let request = match provider.as_str() { "github" | "gitea" => { if repository_name.split('/').count() != 2 { return Err("Invalid repository name.".into()); } if provider == "github" { client.get(format!("{}/repos/{repository_name}", github_api_base_url(&base)?)) .bearer_auth(&token).header(ACCEPT, "application/vnd.github+json") } else { client.get(format!("{base}/api/v1/repos/{repository_name}")) .header("Authorization", format!("token {token}")) } }, "gitlab" | "gitlab-self-hosted" => { if repository_id.is_empty() { return Err("Invalid project identifier.".into()); } client.get(format!("{base}/api/v4/projects/{repository_id}")).header("PRIVATE-TOKEN", &token) }, _ => return Err("Unsupported integration provider.".into()), }; let response = request.header(USER_AGENT, "Gitty").send().map_err(|err| format!("Could not load merge options: {err}"))?; if !response.status().is_success() { return Err(response_error(response, &provider)); } let repository = response.json::().map_err(|err| format!("Could not read merge options: {err}"))?; merge_options(&provider, &repository) })).await.map_err(|_| "The integration API did not respond within 35 seconds.".to_string())? .map_err(|err| format!("Could not load merge options: {err}"))? } #[cfg(test)] mod tests { use super::*; #[test] fn repository_settings_filter_methods_and_select_allowed_default() { let options = merge_options("gitea", &serde_json::json!({"allow_merge_commits":false,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"default_merge_style":"squash"})).unwrap(); assert_eq!(options.methods, ["rebase", "rebase-merge", "squash"]); assert_eq!(options.default_method, "squash"); let options = merge_options("github", &serde_json::json!({"allow_squash_merge":true})).unwrap(); assert_eq!(options.methods, ["squash"]); assert_eq!(options.default_method, "squash"); assert!(merge_options("gitea", &serde_json::json!({})).unwrap().methods.is_empty()); } #[test] fn gitlab_respects_required_and_forbidden_squashing() { for (setting, expected) in [("always", "squash"), ("never", "merge"), ("default_on", "squash"), ("default_off", "merge")] { let options = merge_options("gitlab", &serde_json::json!({"squash_option":setting})).unwrap(); assert_eq!(options.default_method, expected); assert_eq!(options.methods.len(), if setting.starts_with("default") { 2 } else { 1 }); } } #[test] fn payloads_use_provider_specific_methods_and_reject_invalid_choices() { for method in ["merge", "rebase", "rebase-merge", "squash", "fast-forward-only"] { assert_eq!(merge_payload("gitea", Some(method)).unwrap()["Do"], method); } assert_eq!(merge_payload("github", Some("rebase")).unwrap()["merge_method"], "rebase"); assert_eq!(merge_payload("azure-devops", Some("rebase-merge")).unwrap()["completionOptions"]["mergeStrategy"], "rebaseMerge"); assert_eq!(merge_payload("azure-devops", Some("merge")).unwrap()["completionOptions"]["mergeStrategy"], "noFastForward"); assert_eq!(merge_payload("gitlab", Some("squash")).unwrap()["squash"], true); assert_eq!(merge_payload("gitlab", Some("merge")).unwrap()["squash"], false); assert!(merge_payload("github", Some("fast-forward-only")).is_err()); assert!(merge_payload("gitlab", Some("rebase")).is_err()); assert!(merge_payload("gitea", Some("manually-merged")).is_err()); } }