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).
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "gitpub-desktop"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "gitpub_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,197 @@
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
gitpub_desktop_lib::run()
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "gitpub-desktop",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.andrewzambazos.gitpub-desktop",
|
||||
"build": {
|
||||
"frontendDist": "../frontend"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "Gitpub Desktop",
|
||||
"width": 1320,
|
||||
"height": 860,
|
||||
"minWidth": 1024,
|
||||
"minHeight": 680
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||