Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod workarounds;
mod state;
mod validate;
mod settings;
mod repo_settings;

#[cfg(feature = "with-git")]
#[allow(unused_imports)]
Expand Down Expand Up @@ -85,6 +86,8 @@ fn build_invoke_handler<R: tauri::Runtime>() -> impl Fn(tauri::ipc::Invoke<R>) -
tauri_commands::git_push,
tauri_commands::get_global_settings,
tauri_commands::set_global_settings,
tauri_commands::get_repo_settings,
tauri_commands::set_repo_settings,
]
}

Expand Down
6 changes: 6 additions & 0 deletions Backend/src/menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ fn build_repository_menu<R: tauri::Runtime>(app: &tauri::App<R>) -> tauri::Resul
let fetch_item = MenuItem::with_id(app, "fetch", "Fetch", true, Some("F5"))?;
let push_item = MenuItem::with_id(app, "push", "Push", true, Some("Ctrl+P"))?;
let commit_item = MenuItem::with_id(app, "commit", "Commit", true, Some("Ctrl+Enter"))?;
let repo_settings_item = MenuItem::with_id(app, "repo-settings", "Repository Settings", true, None::<&str>)?;
menu::SubmenuBuilder::new(app, "Repository")
.item(&fetch_item)
.item(&push_item)
.item(&commit_item)
.item(&repo_settings_item)
.build()
}

Expand Down Expand Up @@ -107,6 +109,10 @@ pub fn handle_menu_event<R: tauri::Runtime>(app: &tauri::AppHandle<R>, event: Me
// Tell the webview to open the Settings modal
let _ = app.emit("ui:open-settings", ());
}
"repo-settings" => {
// Tell the webview to open the Settings modal
let _ = app.emit("ui:open-repo-settings", ());
}
_ => {
// Fallback: forward other menu IDs if you already rely on this
let _ = app.emit("menu", id);
Expand Down
20 changes: 20 additions & 0 deletions Backend/src/repo_settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoConfig {
/// Repository-local user.name (if set)
#[serde(skip_serializing_if = "Option::is_none")]
pub user_name: Option<String>,
/// Repository-local user.email (if set)
#[serde(skip_serializing_if = "Option::is_none")]
pub user_email: Option<String>,
/// Convenience: the URL for the 'origin' remote (if present)
#[serde(skip_serializing_if = "Option::is_none")]
pub origin_url: Option<String>,
}

impl Default for RepoConfig {
fn default() -> Self {
Self { user_name: None, user_email: None, origin_url: None }
}
}
5 changes: 4 additions & 1 deletion Backend/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ impl Default for General {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Git {
#[serde(default)] pub backend: GitBackend,
/// Default branch name used when creating new repos or inferring defaults
#[serde(default)] pub default_branch: String,
#[serde(default)] pub auto_fetch: bool,
#[serde(default)] pub auto_fetch_minutes: u32,
#[serde(default)] pub prune_on_fetch: bool,
Expand All @@ -80,6 +82,7 @@ impl Default for Git {
fn default() -> Self {
Self {
backend: GitBackend::System,
default_branch: "main".into(),
auto_fetch: true,
auto_fetch_minutes: 30,
prune_on_fetch: true,
Expand Down Expand Up @@ -448,4 +451,4 @@ impl AppConfig {
self.network.http_low_speed_limit =
self.network.http_low_speed_limit.clamp(128, 10_000_000);
}
}
}
16 changes: 16 additions & 0 deletions Backend/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use parking_lot::RwLock;

use openvcs_core::Repo;
use crate::settings::AppConfig;
use crate::repo_settings::RepoConfig;

/// Central application state.
/// Keeps track of the currently open repo and MRU recents.
Expand All @@ -14,6 +15,9 @@ pub struct AppState {
/// Global settings (loaded on startup), thread-safe.
config: RwLock<AppConfig>,

/// Repository-specific settings (in-memory for now)
repo_config: RwLock<RepoConfig>,

/// Currently open repository
current_repo: RwLock<Option<Arc<Repo>>>,

Expand All @@ -26,6 +30,7 @@ impl AppState {
let cfg = AppConfig::load_or_default(); // reads ~/.config/openvcs/openvcs.conf
Self {
config: RwLock::new(cfg),
repo_config: RwLock::new(RepoConfig::default()),
..Default::default()
}
}
Expand Down Expand Up @@ -61,6 +66,17 @@ impl AppState {
Ok(())
}

/* -------- repo config -------- */

pub fn repo_config(&self) -> RepoConfig {
self.repo_config.read().clone()
}

pub fn set_repo_config(&self, cfg: RepoConfig) -> Result<(), String> {
*self.repo_config.write() = cfg;
Ok(())
}

/// Transactional edit: clone → mutate → validate → save → swap.
/// Keep the closure FAST (no blocking/async in here).
pub fn edit_config<F>(&self, f: F) -> Result<(), String>
Expand Down
73 changes: 70 additions & 3 deletions Backend/src/tauri_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use openvcs_core::{OnEvent, models::{BranchItem, StatusPayload, CommitItem}, Rep
use openvcs_core::backend_descriptor::{get_backend, list_backends};
use openvcs_core::models::{VcsEvent};
use crate::settings::AppConfig;
use crate::repo_settings::RepoConfig;

#[derive(serde::Serialize)]
struct RepoSelectedPayload {
Expand Down Expand Up @@ -503,9 +504,18 @@ pub async fn commit_changes<R: Runtime>(
on(VcsEvent::Info("Staging changes…"));
info!("Staging changes for commit");

// Backend-agnostic identity fallback; wire a real identity source later.
let name = std::env::var("GIT_AUTHOR_NAME").unwrap_or_else(|_| "OpenVCS".into());
let email = std::env::var("GIT_AUTHOR_EMAIL").unwrap_or_else(|_| "openvcs@example".into());
// Resolve identity: prefer VCS-reported (repo-local, then global), then env, then final fallback
let (name, email) = repo
.inner()
.get_identity()
.ok()
.flatten()
.or_else(|| {
let n = std::env::var("GIT_AUTHOR_NAME").ok();
let e = std::env::var("GIT_AUTHOR_EMAIL").ok();
match (n, e) { (Some(n), Some(e)) if !n.is_empty() && !e.is_empty() => Some((n, e)), _ => None }
})
.unwrap_or_else(|| ("OpenVCS".into(), "openvcs@example".into()));
info!("Using identity: {} <{}>", name, email);

on(VcsEvent::Info("Writing commit…"));
Expand Down Expand Up @@ -693,3 +703,60 @@ pub fn set_global_settings(
) -> Result<(), String> {
state.set_config(cfg)
}

#[tauri::command]
pub fn get_repo_settings(state: State<'_, AppState>) -> Result<RepoConfig, String> {
let mut cfg = state.repo_config();
// If a repo is open, enrich settings from actual Git config
if let Some(repo) = state.current_repo() {
let vcs = repo.inner();
// identity (repository-local)
match vcs.get_identity() {
Ok(Some((name, email))) => {
cfg.user_name = Some(name);
cfg.user_email = Some(email);
}
Ok(None) => { /* leave as-is */ }
Err(e) => {
warn!("get_repo_settings: get_identity failed: {e}");
}
}

// remotes: capture 'origin' URL if present
match vcs.list_remotes() {
Ok(list) => {
if let Some((_, url)) = list.into_iter().find(|(n, _)| n == "origin") {
cfg.origin_url = Some(url);
}
}
Err(e) => warn!("get_repo_settings: list_remotes failed: {e}"),
}
}

Ok(cfg)
}

#[tauri::command]
pub fn set_repo_settings(
state: State<'_, AppState>,
cfg: RepoConfig,
) -> Result<(), String> {
// Persist repo-specific cache (none currently persisted beyond identity/remote)
state.set_repo_config(RepoConfig { ..cfg.clone() })?;

// Apply to Git if a repo is open
if let Some(repo) = state.current_repo() {
let vcs = repo.inner();
// Identity: set when both present
if let (Some(name), Some(email)) = (cfg.user_name.as_deref(), cfg.user_email.as_deref()) {
vcs.set_identity_local(name, email).map_err(|e| e.to_string())?;
}
// Origin remote URL
if let Some(url) = cfg.origin_url.as_deref() {
if !url.trim().is_empty() {
vcs.ensure_remote("origin", url).map_err(|e| e.to_string())?;
}
}
}
Ok(())
}
28 changes: 28 additions & 0 deletions Frontend/src/modals/repo-settings.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div class="modal" id="repo-settings-modal" aria-hidden="true">
<div class="dialog sheet" role="dialog" aria-modal="true" aria-labelledby="repo-settings-title">
<div class="sheet-head">
<h3 id="repo-settings-title" style="margin:0">Repository Settings</h3>
</div>
<div class="sheet-body">
<form id="repo-settings-form" class="panel-form">
<div class="group">
<label for="git-user-name">Git user.name (repo-local)</label>
<input id="git-user-name" type="text" />
</div>
<div class="group">
<label for="git-user-email">Git user.email (repo-local)</label>
<input id="git-user-email" type="email" />
</div>
<div class="group">
<label for="git-origin-url">Origin remote URL</label>
<input id="git-origin-url" type="text" placeholder="git@host:org/repo.git or https://…" />
</div>
</form>
</div>
<div class="sheet-actions">
<button class="tbtn" type="button" data-close>Cancel</button>
<button class="tbtn primary big" id="repo-settings-save" type="button">Save</button>
</div>
</div>
<div class="backdrop" data-close></div>
</div>
40 changes: 40 additions & 0 deletions Frontend/src/scripts/features/repoSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { TAURI } from '../lib/tauri';
import { openModal, closeModal } from '../ui/modals';
import { notify } from '../lib/notify';
import type { RepoSettings } from '../types';

export function openRepoSettings(){ openModal('repo-settings-modal'); }

export async function wireRepoSettings() {
const modal = document.getElementById('repo-settings-modal') as HTMLElement | null;
if (!modal || (modal as any).__wired) return;
(modal as any).__wired = true;

const nameInput = modal.querySelector('#git-user-name') as HTMLInputElement | null;
const emailInput = modal.querySelector('#git-user-email') as HTMLInputElement | null;
const originInput= modal.querySelector('#git-origin-url') as HTMLInputElement | null;
const saveBtn = modal.querySelector('#repo-settings-save') as HTMLButtonElement | null;

if (TAURI.has) {
try {
const cfg = await TAURI.invoke<RepoSettings>('get_repo_settings');
if (nameInput && cfg?.user_name) nameInput.value = cfg.user_name;
if (emailInput && cfg?.user_email) emailInput.value = cfg.user_email;
if (originInput && cfg?.origin_url) originInput.value = cfg.origin_url;
} catch { /* ignore */ }
}

saveBtn?.addEventListener('click', async () => {
const next: RepoSettings = {
user_name: nameInput?.value || undefined,
user_email: emailInput?.value || undefined,
origin_url: originInput?.value || undefined,
};
try {
if (TAURI.has) await TAURI.invoke('set_repo_settings', { cfg: next });
closeModal('repo-settings-modal');
} catch {
notify('Failed to save repository settings');
}
});
}
2 changes: 1 addition & 1 deletion Frontend/src/scripts/features/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function wireSettings() {
const cur = await TAURI.invoke<GlobalSettings>('get_global_settings');

cur.general = { theme: 'system', language: 'system', update_channel: 'stable', reopen_last_repos: true, checks_on_launch: true, telemetry: false, crash_reports: false };
cur.git = { backend: 'git-system', auto_fetch: true, auto_fetch_minutes: 30, prune_on_fetch: true, watcher_debounce_ms: 300, large_repo_threshold_mb: 500, allow_hooks: 'ask', respect_core_autocrlf: true };
cur.git = { backend: 'git-system', default_branch: 'main', auto_fetch: true, auto_fetch_minutes: 30, prune_on_fetch: true, watcher_debounce_ms: 300, large_repo_threshold_mb: 500, allow_hooks: 'ask', respect_core_autocrlf: true };
cur.diff = { tab_width: 4, ignore_whitespace: 'none', max_file_size_mb: 10, intraline: true, show_binary_placeholders: true, external_diff: {enabled:false,path:'',args:''}, external_merge: {enabled:false,path:'',args:''}, binary_exts: ['png','jpg','dds','uasset'] };
cur.lfs = { enabled: true, concurrency: 4, bandwidth_kbps: 0, require_lock_before_edit: false, background_fetch_on_checkout: true };
cur.performance = { graph_node_cap: 5000, progressive_render: true, gpu_accel: true, index_warm_on_open: true, background_index_on_battery: false };
Expand Down
8 changes: 5 additions & 3 deletions Frontend/src/scripts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { bindCommit } from './features/diff';
import { openAbout } from './features/about';
import { openModal } from './ui/modals';
import { openSettings, loadSettingsIntoForm } from './features/settings';
import { openRepoSettings } from './features/repoSettings';

// Title bar actions
const themeBtn = qs<HTMLButtonElement>('#theme-btn');
Expand Down Expand Up @@ -116,8 +117,9 @@ function boot() {
})();

// open settings via event
TAURI.listen?.('ui:open-settings', () => openModal('settings-modal'));
TAURI.listen?.('ui:open-about', () => openAbout());
}
TAURI.listen?.('ui:open-settings', () => openModal('settings-modal'));
TAURI.listen?.('ui:open-about', () => openAbout());
TAURI.listen?.('ui:open-repo-settings', () => openRepoSettings());
}

boot();
7 changes: 7 additions & 0 deletions Frontend/src/scripts/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface GlobalSettings {
};
git?: {
backend?: 'git-system'|'libgit2'|string;
default_branch?: string;
auto_fetch?: boolean;
auto_fetch_minutes?: number;
prune_on_fetch?: boolean;
Expand Down Expand Up @@ -80,3 +81,9 @@ export interface GlobalSettings {
color_blind_mode?: string;
};
}

export interface RepoSettings {
user_name?: string;
user_email?: string;
origin_url?: string;
}
4 changes: 4 additions & 0 deletions Frontend/src/scripts/ui/modals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import settingsHtml from "@modals/settings.html?raw";
import cmdHtml from "@modals/commandSheet.html?raw";
import aboutHtml from "@modals/about.html?raw";
import { wireSettings } from "../features/settings";
import repoSettingsHtml from "@modals/repo-settings.html?raw";
import { wireRepoSettings } from "../features/repoSettings";

// Lazy fragments (only those NOT present at load)
const FRAGMENTS: Record<string, string> = {
"settings-modal": settingsHtml,
"about-modal": aboutHtml,
"command-modal": cmdHtml,
"repo-settings-modal": repoSettingsHtml,
};

const loaded = new Set<string>();
Expand Down Expand Up @@ -45,6 +48,7 @@ export function hydrate(id: string): void {
loaded.add(id);

if (id === "settings-modal") wireSettings();
if (id === "repo-settings-modal") wireRepoSettings();
}

export function openModal(id: string): void {
Expand Down
1 change: 1 addition & 0 deletions Frontend/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
@import "./modal/modal-base.css";
@import "./modal/command-sheet.css";
@import "./modal/settings.css";
@import "./modal/repo-settings.css";

/* Media queries last */
@import "./responsive.css";
3 changes: 2 additions & 1 deletion Frontend/src/styles/modal/modal-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
}
.modal[aria-hidden="true"]{ display:none; }

.backdrop{ position:absolute; inset:0; background:rgba(0,0,0,.55); }
.backdrop{ position:absolute; inset:0; background:rgba(0,0,0,.55); z-index:0; }

.dialog.sheet{
position:relative; overflow:hidden;
z-index:1; /* ensure dialog renders above backdrop */
background:var(--surface); color:var(--text);
border:1px solid var(--border); border-radius:var(--r-lg); box-shadow:var(--shadow-1);
}
Expand Down
Loading
Loading