From a3ca78fced7b76f9430ea1dca0de74667e5f2612 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 28 Apr 2026 02:38:32 +0200 Subject: [PATCH] fix: guard git init parent folders --- docs/ABSTRACTIONS.md | 2 + src-tauri/src/commands/git.rs | 79 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index eb84aa77..911c166e 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -68,6 +68,8 @@ Git is a per-vault capability, not a prerequisite for the document model. A vaul Plain folders become Git-backed only when the user explicitly runs Git initialization from the setup dialog, status bar, or command palette. Features that depend on Git must check this capability instead of assuming every vault has `.git`. +Git initialization is intentionally scoped to dedicated vault folders. When the current non-git folder looks like a broad personal root such as Documents, Desktop, or Downloads and does not already carry Tolaria-managed vault markers, `init_git_repo` refuses to run Git and asks the user to select or create a dedicated subfolder instead. + ### VaultEntry The core data type representing a single note, defined in Rust (`src-tauri/src/vault/mod.rs`) and TypeScript (`src/types.ts`). diff --git a/src-tauri/src/commands/git.rs b/src-tauri/src/commands/git.rs index 2b9eb92a..591c5ff3 100644 --- a/src-tauri/src/commands/git.rs +++ b/src-tauri/src/commands/git.rs @@ -158,10 +158,62 @@ pub fn is_git_repo(vault_path: VaultPathArg) -> bool { .is_dir() } +#[cfg(desktop)] +fn validate_git_init_target(vault_path: &str) -> Result<(), String> { + let path = std::path::Path::new(vault_path); + if !path.exists() { + return Err("Choose an existing vault folder before initializing Git".to_string()); + } + if !path.is_dir() { + return Err("Choose a folder before initializing Git".to_string()); + } + + if is_broad_personal_folder(path) && !has_tolaria_vault_marker(path) { + return Err(format!( + "Choose a dedicated vault folder before initializing Git. '{}' looks like a broad personal folder; create or select a subfolder such as '{}' instead.", + path.display(), + path.join("Tolaria").display() + )); + } + + Ok(()) +} + +#[cfg(desktop)] +fn is_broad_personal_folder(path: &std::path::Path) -> bool { + let Some(name) = path.file_name().and_then(|name| name.to_str()) else { + return false; + }; + + matches!( + name.to_ascii_lowercase().as_str(), + "desktop" + | "documents" + | "downloads" + | "movies" + | "music" + | "pictures" + | "public" + | "templates" + | "videos" + ) +} + +#[cfg(desktop)] +fn has_tolaria_vault_marker(path: &std::path::Path) -> bool { + ["AGENTS.md", "CLAUDE.md", "type.md", "note.md"] + .iter() + .any(|file| path.join(file).is_file()) + || ["attachments", "type", "views"] + .iter() + .any(|dir| path.join(dir).is_dir()) +} + #[cfg(desktop)] #[tauri::command] pub fn init_git_repo(vault_path: VaultPathArg) -> Result<(), String> { let vault_path = expand_tilde(&vault_path); + validate_git_init_target(&vault_path)?; crate::git::init_repo(&vault_path) } @@ -374,6 +426,33 @@ mod tests { ); } + #[test] + fn init_git_repo_rejects_broad_personal_folders() { + let dir = TempDir::new().unwrap(); + let documents = dir.path().join("Documents"); + fs::create_dir_all(&documents).unwrap(); + fs::write(documents.join("unrelated.txt"), "not a vault").unwrap(); + + let err = init_git_repo(documents.to_string_lossy().into_owned()) + .expect_err("expected Documents itself to be rejected before git init"); + + assert!(err.contains("dedicated vault folder")); + assert!(!documents.join(".git").exists()); + } + + #[test] + fn init_git_repo_allows_named_vault_subfolder_under_documents() { + let dir = TempDir::new().unwrap(); + let vault = dir.path().join("Documents").join("Tolaria"); + fs::create_dir_all(&vault).unwrap(); + fs::write(vault.join("note.md"), "# Note\n").unwrap(); + let vault = vault.to_string_lossy().into_owned(); + + init_git_repo(vault.clone()).unwrap(); + + assert!(is_git_repo(vault)); + } + #[tokio::test] async fn desktop_remote_commands_report_no_remote() { let (_dir, vault) = create_initialized_vault();