use reqwest::blocking::Client; use reqwest::header::{ACCEPT, AUTHORIZATION}; use serde::Serialize; use std::path::Path; use std::process::Command; #[derive(Serialize)] struct GitCommandResult { command: String, stdout: String, stderr: String, success: bool, } #[derive(Serialize)] struct ServerConnectionResult { ok: bool, message: String, api_base_url: String, version: Option, } fn normalize_api_base_url(server_url: &str) -> Result { // Normalize user input so every backend consistently resolves to /api/v1. let trimmed = server_url.trim().trim_end_matches('/'); if !(trimmed.starts_with("http://") || trimmed.starts_with("https://")) { return Err("Server URL must start with http:// or https://".to_string()); } if trimmed.ends_with("/api/v1") { Ok(trimmed.to_string()) } else { Ok(format!("{trimmed}/api/v1")) } } fn resolve_git_binary(git_path: Option) -> String { git_path .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .unwrap_or("git") .to_string() } fn run_git_command( repo_path: Option<&str>, git_path: Option, args: Vec, ) -> Result { // Central command executor keeps Git command behavior consistent. let git_binary = resolve_git_binary(git_path); let mut command = Command::new(&git_binary); if let Some(path) = repo_path { if !Path::new(path).exists() { return Err(format!("Repository path does not exist: {path}")); } command.current_dir(path); } command.args(&args); let output = command .output() .map_err(|err| format!("Failed to run git command: {err}"))?; Ok(GitCommandResult { command: format!("{git_binary} {}", args.join(" ")), stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(), stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(), success: output.status.success(), }) } #[tauri::command] fn git_clone( repo_url: String, destination_path: String, git_path: Option, ) -> Result { if repo_url.trim().is_empty() { return Err("Repository URL is required".to_string()); } if destination_path.trim().is_empty() { return Err("Destination path is required".to_string()); } run_git_command( None, git_path, vec![ "clone".to_string(), repo_url.trim().to_string(), destination_path.trim().to_string(), ], ) } #[tauri::command] fn git_pull(repo_path: String, git_path: Option) -> Result { run_git_command(Some(repo_path.trim()), git_path, vec!["pull".to_string()]) } #[tauri::command] fn git_push(repo_path: String, git_path: Option) -> Result { run_git_command(Some(repo_path.trim()), git_path, vec!["push".to_string()]) } #[tauri::command] fn git_status(repo_path: String, git_path: Option) -> Result { run_git_command( Some(repo_path.trim()), git_path, vec!["status".to_string(), "--short".to_string(), "--branch".to_string()], ) } #[tauri::command] fn git_branch(repo_path: String, git_path: Option) -> Result { run_git_command( Some(repo_path.trim()), git_path, vec!["branch".to_string(), "--all".to_string()], ) } #[tauri::command] fn test_gitea_connection( server_url: String, auth_method: String, token: Option, username: Option, password: Option, ) -> Result { // A lightweight compatibility check against the canonical Gitea version endpoint. let api_base_url = normalize_api_base_url(&server_url)?; let version_url = format!("{api_base_url}/version"); let mut request = Client::new() .get(&version_url) .header(ACCEPT, "application/json"); if auth_method == "token" { if let Some(value) = token.as_deref() { if !value.trim().is_empty() { request = request.header(AUTHORIZATION, format!("token {}", value.trim())); } } } else if auth_method == "password" { request = request.basic_auth(username.unwrap_or_default(), password); } let response = request .send() .map_err(|err| format!("Failed to connect to Gitea server: {err}"))?; if !response.status().is_success() { return Ok(ServerConnectionResult { ok: false, message: format!("Server responded with status {}", response.status()), api_base_url, version: None, }); } let response_json: serde_json::Value = response .json() .map_err(|err| format!("Failed to parse Gitea response: {err}"))?; let version = response_json .get("version") .and_then(|value| value.as_str()) .map(ToString::to_string); Ok(ServerConnectionResult { ok: true, message: "Gitea API is reachable and compatible.".to_string(), api_base_url, version, }) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![ git_clone, git_pull, git_push, git_status, git_branch, test_gitea_connection ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }