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
4 changes: 4 additions & 0 deletions Backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ fn build_invoke_handler<R: tauri::Runtime>() -> impl Fn(tauri::ipc::Invoke<R>) -
tauri_commands::git_push,
tauri_commands::git_undo_since_push,
tauri_commands::git_undo_to_commit,
tauri_commands::git_lfs_fetch_all,
tauri_commands::git_lfs_pull,
tauri_commands::git_lfs_prune,
tauri_commands::git_lfs_track_paths,
tauri_commands::get_global_settings,
tauri_commands::set_global_settings,
tauri_commands::get_repo_settings,
Expand Down
31 changes: 30 additions & 1 deletion Backend/src/menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ const WIKI_URL: &str = "https://github.com/jordonbc/OpenVCS/wiki";
pub fn build_and_attach_menu<R: tauri::Runtime>(app: &tauri::App<R>) -> tauri::Result<()> {
let file_menu = build_file_menu(app)?;
let repo_menu = build_repository_menu(app)?;
let lfs_menu = build_lfs_menu(app)?;
let help_menu = build_help_menu(app)?;

let menu: Menu<R> = MenuBuilder::new(app)
.items(&[&file_menu, &repo_menu, &help_menu])
.items(&[&file_menu, &repo_menu, &lfs_menu, &help_menu])
.build()?;

app.set_menu(menu)?;
Expand Down Expand Up @@ -80,6 +81,22 @@ fn build_repository_menu<R: tauri::Runtime>(app: &tauri::App<R>) -> tauri::Resul
.build()
}

/// ----- LFS -----
fn build_lfs_menu<R: tauri::Runtime>(app: &tauri::App<R>) -> tauri::Result<menu::Submenu<R>> {
let pull_item = MenuItem::with_id(app, "lfs-pull-all", "Download LFS Files", true, None::<&str>)?;
let fetch_item = MenuItem::with_id(app, "lfs-fetch-all", "Fetch LFS", true, None::<&str>)?;
let prune_item = MenuItem::with_id(app, "lfs-prune", "Prune LFS Cache", true, None::<&str>)?;
let settings_item = MenuItem::with_id(app, "lfs-settings", "LFS Preferences…", true, None::<&str>)?;

menu::SubmenuBuilder::new(app, "LFS")
.item(&pull_item)
.item(&fetch_item)
.item(&prune_item)
.separator()
.item(&settings_item)
.build()
}

/// ----- Help -----
fn build_help_menu<R: tauri::Runtime>(app: &tauri::App<R>) -> tauri::Result<menu::Submenu<R>> {
let docs_item = MenuItem::with_id(app, "docs", "Documentation", true, None::<&str>)?;
Expand Down Expand Up @@ -110,6 +127,18 @@ pub fn handle_menu_event<R: tauri::Runtime>(app: &tauri::AppHandle<R>, event: Me
"repo-edit-gitattributes" => {
open_repo_dotfile(app, ".gitattributes");
}
"lfs-pull-all" => {
let _ = app.emit("menu", "lfs-pull-all");
}
"lfs-fetch-all" => {
let _ = app.emit("menu", "lfs-fetch-all");
}
"lfs-prune" => {
let _ = app.emit("menu", "lfs-prune");
}
"lfs-settings" => {
let _ = app.emit("ui:open-settings", serde_json::json!({"section":"lfs"}));
}
"add_repo" => {
let app_cloned = app.clone();
async_runtime::spawn(async move {
Expand Down
126 changes: 125 additions & 1 deletion Backend/src/tauri_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use openvcs_core::{OnEvent, models::{BranchItem, StatusPayload, CommitItem, Stas
use serde::Serialize;
use openvcs_core::backend_descriptor::{get_backend, list_backends};
use openvcs_core::models::{VcsEvent};
use crate::settings::AppConfig;
use crate::settings::{AppConfig, Lfs};
use crate::repo_settings::RepoConfig;
use tauri_plugin_updater::UpdaterExt;

Expand Down Expand Up @@ -42,6 +42,61 @@ struct ProgressPayload {
message: String
}

fn lfs_config(state: &State<'_, AppState>) -> Lfs {
state.with_config(|cfg| cfg.lfs.clone())
}

struct LfsEnvGuard {
originals: Vec<(&'static str, Option<String>)>,
}

impl LfsEnvGuard {
fn capture(key: &'static str) -> Option<String> {
std::env::var(key).ok()
}

fn set(key: &'static str, value: Option<String>, originals: &mut Vec<(&'static str, Option<String>)>) {
originals.push((key, Self::capture(key)));
match value {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}

fn apply(cfg: &Lfs) -> Self {
let mut originals = Vec::new();

// Concurrency controls parallel transfers; always set when enabled.
Self::set(
"GIT_LFS_CONCURRENCY",
Some(cfg.concurrency.clamp(1, 16).to_string()),
&mut originals,
);

// Require lock → respect read-only enforcement so accidental edits are blocked.
let lock_env = if cfg.require_lock_before_edit { Some("1".to_string()) } else { None };
Self::set("GIT_LFS_SET_LOCKED_FILES_READONLY", lock_env, &mut originals);

// Background fetch flag toggles smudge behaviour. When disabled, skip smudge to avoid slow checkouts.
let skip_smudge = if cfg.background_fetch_on_checkout { None } else { Some("1".to_string()) };
Self::set("GIT_LFS_SKIP_SMUDGE", skip_smudge, &mut originals);

Self { originals }
}
}

impl Drop for LfsEnvGuard {
fn drop(&mut self) {
for (key, val) in self.originals.drain(..).rev() {
if let Some(v) = val {
std::env::set_var(key, v);
} else {
std::env::remove_var(key);
}
}
}
}

#[tauri::command]
pub fn about_info() -> utilities::AboutInfo {
utilities::AboutInfo::gather()
Expand Down Expand Up @@ -979,6 +1034,75 @@ pub fn git_pull<R: Runtime>(window: Window<R>, state: State<'_, AppState>) -> Re
Ok(())
}

#[tauri::command]
pub fn git_lfs_fetch_all(state: State<'_, AppState>) -> Result<(), String> {
info!("git_lfs_fetch_all called");
let repo = state
.current_repo()
.ok_or_else(|| "No repository selected".to_string())?;

let cfg = lfs_config(&state);
if !cfg.enabled {
return Err("Git LFS integration is disabled".into());
}

let _guard = LfsEnvGuard::apply(&cfg);
repo.inner().lfs_fetch().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn git_lfs_pull(state: State<'_, AppState>) -> Result<(), String> {
info!("git_lfs_pull called");
let repo = state
.current_repo()
.ok_or_else(|| "No repository selected".to_string())?;

let cfg = lfs_config(&state);
if !cfg.enabled {
return Err("Git LFS integration is disabled".into());
}

let _guard = LfsEnvGuard::apply(&cfg);
repo.inner().lfs_pull().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn git_lfs_prune(state: State<'_, AppState>) -> Result<(), String> {
info!("git_lfs_prune called");
let repo = state
.current_repo()
.ok_or_else(|| "No repository selected".to_string())?;

let cfg = lfs_config(&state);
if !cfg.enabled {
return Err("Git LFS integration is disabled".into());
}

let _guard = LfsEnvGuard::apply(&cfg);
repo.inner().lfs_prune().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn git_lfs_track_paths(state: State<'_, AppState>, paths: Vec<String>) -> Result<(), String> {
info!("git_lfs_track_paths called (count={})", paths.len());
if paths.is_empty() {
return Ok(());
}

let repo = state
.current_repo()
.ok_or_else(|| "No repository selected".to_string())?;

let cfg = lfs_config(&state);
if !cfg.enabled {
return Err("Git LFS integration is disabled".into());
}

let _guard = LfsEnvGuard::apply(&cfg);
let list: Vec<PathBuf> = paths.into_iter().map(PathBuf::from).collect();
repo.inner().lfs_track(&list).map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn git_push<R: Runtime>(
window: Window<R>,
Expand Down
8 changes: 4 additions & 4 deletions Frontend/src/modals/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,26 +175,26 @@ <h3 id="settings-title" style="margin:0">Settings</h3>
<!-- LFS -->
<form class="panel-form hidden" data-panel="lfs">
<div class="group">
<label class="checkbox"><input type="checkbox" id="set-lfs-enabled" disabled /> Enable LFS integration
<label class="checkbox"><input type="checkbox" id="set-lfs-enabled" /> Enable LFS integration
<span class="help-tip" title="Support Git LFS for large files (download pointers as real files).">?</span>
</label>
</div>
<div class="group">
<label for="set-lfs-concurrency">Concurrent transfers
<span class="help-tip" title="Number of parallel LFS uploads/downloads.">?</span>
</label>
<input id="set-lfs-concurrency" type="number" min="1" max="16" disabled />
<input id="set-lfs-concurrency" type="number" min="1" max="16" />
</div>
<div class="group">

</div>
<div class="group">
<label class="checkbox"><input type="checkbox" id="set-lfs-require-lock" disabled /> Require lock before edit
<label class="checkbox"><input type="checkbox" id="set-lfs-require-lock" /> Require lock before edit
<span class="help-tip" title="Prevent conflicting edits by requiring an LFS lock.">?</span>
</label>
</div>
<div class="group">
<label class="checkbox"><input type="checkbox" id="set-lfs-bg-fetch" disabled /> Background fetch on checkout
<label class="checkbox"><input type="checkbox" id="set-lfs-bg-fetch" /> Background fetch on checkout
<span class="help-tip" title="Fetch LFS objects automatically after switching branches.">?</span>
</label>
</div>
Expand Down
6 changes: 3 additions & 3 deletions Frontend/src/scripts/features/branches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { notify } from '../lib/notify';
import { state } from '../state/state';
import { openModal } from '../ui/modals';
import { openRenameBranch } from './renameBranch';
import { buildCtxMenu } from '../lib/menu';
import { buildCtxMenu, CtxItem } from '../lib/menu';
import { renderList } from './repo';

type Branch = { name: string; current?: boolean; kind?: { type?: string; remote?: string } };
Expand Down Expand Up @@ -114,7 +114,7 @@ export function bindBranchUI() {
const b = (state.branches || []).find(br => br.name === name) as Branch | undefined;
const kind = b?.kind?.type?.toLowerCase() || 'local';
const wantForce = Boolean(e.shiftKey);
const items: { label: string; action: () => void }[] = [];
const items: CtxItem[] = [];
items.push({ label: 'Checkout', action: async () => {
try { if (TAURI.has) await TAURI.invoke('git_checkout_branch', { name }); await loadBranches(); notify(`Switched to ${name}`); renderList(); }
catch { notify('Checkout failed'); }
Expand All @@ -127,7 +127,7 @@ export function bindBranchUI() {
catch { notify('Merge failed'); }
}});
if (kind !== 'remote') {
items.push({ label: '---', action: () => {} });
items.push({ label: '---' });
items.push({ label: 'Rename…', action: () => openRenameBranch(name) });
items.push({ label: wantForce ? 'Force delete…' : 'Delete…', action: async () => {
if (name === cur) { notify('Cannot delete the current branch'); return; }
Expand Down
32 changes: 25 additions & 7 deletions Frontend/src/scripts/features/repo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// src/scripts/features/repo.ts
import { qs, qsa, escapeHtml } from '../lib/dom';
import { buildCtxMenu } from '../lib/menu';
import { buildCtxMenu, CtxItem } from '../lib/menu';
import { TAURI } from '../lib/tauri';
import { notify } from '../lib/notify';
import { state, prefs, statusLabel, statusClass } from '../state/state';
Expand Down Expand Up @@ -120,7 +120,7 @@ export function renderList() {
li.addEventListener('contextmenu', (ev) => {
ev.preventDefault();
const x = (ev as MouseEvent).clientX, y = (ev as MouseEvent).clientY;
const items: any[] = [];
const items: CtxItem[] = [];
// Always offer copy hash for discoverability so menu shows
items.push({ label: 'Copy hash', action: async () => {
try {
Expand All @@ -129,7 +129,7 @@ export function renderList() {
} catch { /* ignore */ }
}});
if (isAhead) {
items.push('---');
items.push({ label: '---' });
items.push({ label: 'Undo to this commit', action: async () => {
if (!TAURI.has) return;
try {
Expand All @@ -138,7 +138,7 @@ export function renderList() {
} catch { notify('Undo failed'); }
}});
}
buildCtxMenu(items as any, x, y);
buildCtxMenu(items, x, y);
});
listEl.appendChild(li);
});
Expand Down Expand Up @@ -183,7 +183,7 @@ export function renderList() {
const mev = ev as MouseEvent;
const x = mev.clientX, y = mev.clientY;
const target = sel;
const items: { label: string; action: () => void }[] = [];
const items: CtxItem[] = [];
items.push({ label: 'Apply stash', action: async () => {
try {
if (!TAURI.has) return;
Expand All @@ -205,7 +205,7 @@ export function renderList() {
renderList();
} catch { notify('Failed to delete stash'); }
}});
buildCtxMenu(items as any, x, y);
buildCtxMenu(items, x, y);
});
listEl.appendChild(li);
});
Expand Down Expand Up @@ -969,7 +969,7 @@ function onFileContextMenu(ev: MouseEvent, f: { path: string }) {
const clickedInSelection = manualSelection.includes(f.path);
const hasMultiSelection = manualSelection.length > 1 && clickedInSelection;
const hasSingleSelection = manualSelection.length === 1 && clickedInSelection;
const items: { label: string; action: () => void }[] = [];
const items: CtxItem[] = [];
const openStashForPaths = (paths: string[], defaultMessage: string) => {
if (!paths.length) return;
openStashConfirm({
Expand Down Expand Up @@ -1009,6 +1009,24 @@ function onFileContextMenu(ev: MouseEvent, f: { path: string }) {
items.push({ label: 'Create stash for this file…', action: () => {
openStashForPaths([singleTarget], defaultMsg);
}});
items.push({ label: '---' });
items.push({ label: 'Track with Git LFS', action: () => {
if (!TAURI.has) {
notify('Git LFS is available in the desktop app');
return;
}
const targets = (hasManualSelection && clickedInSelection ? manualSelection.slice() : [f.path]).filter(Boolean);
if (!targets.length) return;
(async () => {
try {
await TAURI.invoke('git_lfs_track_paths', { paths: targets });
notify(targets.length > 1 ? 'Tracked files with Git LFS' : 'Tracked file with Git LFS');
await Promise.allSettled([hydrateStatus()]);
} catch {
notify('Git LFS track failed');
}
})();
}});
buildCtxMenu(items, x, y);
}

Expand Down
Loading
Loading