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).
This commit is contained in:
2026-05-07 14:41:15 +12:00
commit 6b245c628c
44 changed files with 7105 additions and 0 deletions
+7
View File
@@ -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
+5456
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -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"] }
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+10
View File
@@ -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"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+197
View File
@@ -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");
}
+6
View File
@@ -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()
}
+35
View File
@@ -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"
]
}
}