Files
Gitpub-Desktop/src-tauri/src/lib.rs
T
andrew 6b245c628c Initial commit: Gitpub Desktop scaffold
Add complete project scaffold for Gitpub Desktop (Tauri + Rust backend and vanilla HTML/CSS/JS frontend). Includes frontend entry (index.html), styles (base/components CSS), app logic and modules (app.js, gitea-api.js, tauri-api.js, state.js, storage.js), static assets and component READMEs, README.md, VSCode recommendations, and project config (package.json, package-lock.json). Also adds src-tauri skeleton (Cargo.toml, main.rs, lib.rs, build.rs, tauri.conf.json), application icons, and .gitignore files. Provides MVP plumbing for server setup and management, repository dashboard, local repo opening, and Git operations via Tauri invoke (clone/pull/push/status/branch).
2026-05-07 14:41:15 +12:00

198 lines
5.6 KiB
Rust

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<String>,
}
fn normalize_api_base_url(server_url: &str) -> Result<String, String> {
// 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>) -> 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<String>,
args: Vec<String>,
) -> Result<GitCommandResult, String> {
// 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<String>,
) -> Result<GitCommandResult, String> {
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<String>) -> Result<GitCommandResult, String> {
run_git_command(Some(repo_path.trim()), git_path, vec!["pull".to_string()])
}
#[tauri::command]
fn git_push(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
run_git_command(Some(repo_path.trim()), git_path, vec!["push".to_string()])
}
#[tauri::command]
fn git_status(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
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<String>) -> Result<GitCommandResult, String> {
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<String>,
username: Option<String>,
password: Option<String>,
) -> Result<ServerConnectionResult, String> {
// 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");
}