diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 2deeb8e7..0dfe13ca 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -741,6 +741,7 @@ Tolaria tracks managed vault-level AI guidance separately from normal note conte Tolaria delegates remote auth to the user's system git setup: - `CloneVaultModal` captures a remote URL and local destination - `clone_git_repo` and `create_getting_started_vault` both run system git clone work in blocking Tokio tasks so clone UIs stay responsive +- On Linux AppImage launches, every system-git command removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` before spawning `git`, so helpers like `git-remote-https` bind against the host git/library stack instead of Tolaria's bundled WebKit/AppImage libraries - `git_add_remote` uses the same system git path and refuses remotes whose history is unrelated or ahead of the local vault - Existing `git_pull` / `git_push` commands keep surfacing raw git errors, and clone commands fail fast when git wants interactive terminal input - No provider-specific token or username is stored in app settings diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bad0094f..b1d2e999 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -510,8 +510,9 @@ Tolaria no longer implements provider-specific OAuth or remote-repository APIs. 1. User opens `CloneVaultModal` from onboarding or the vault menu 2. User pastes any git URL and chooses a local destination 3. The `clone_git_repo()` Tauri command runs `git clone` inside a blocking Tokio task so the Tauri window stays responsive during slow or failing clones -4. `git_push()` / `git_pull()` continue to use the same system git path -5. Clone commands disable interactive terminal / askpass prompts and surface the git failure back to the UI instead of freezing the app waiting for input +4. Linux AppImage builds strip AppImage loader variables from system-git subprocesses before spawning `git`, keeping `git-remote-https` on the host git/library stack +5. `git_push()` / `git_pull()` continue to use the same system git path +6. Clone commands disable interactive terminal / askpass prompts and surface the git failure back to the UI instead of freezing the app waiting for input **Auth model:** - SSH keys, Git Credential Manager, macOS Keychain helpers, `gh auth`, and other git helpers all work without app-specific setup diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 7638382c..a51d5c90 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -69,6 +69,8 @@ pnpm playwright:regression # Full Playwright regression suite `create_getting_started_vault` clones the public starter repo and then removes every git remote from the new local copy. That means Getting Started vaults open local-only by default. Users connect a compatible remote later through the bottom-bar `No remote` chip or the command palette, both of which feed the same `AddRemoteModal` and `git_add_remote` backend flow. +Linux AppImage builds still use the user's system `git`. Before Tolaria spawns that `git` process, it removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` so HTTPS clone helpers use the host git libraries instead of bundled AppImage libraries. + ## Directory Structure ``` diff --git a/src-tauri/src/commands/git.rs b/src-tauri/src/commands/git.rs index 591c5ff3..5579fca8 100644 --- a/src-tauri/src/commands/git.rs +++ b/src-tauri/src/commands/git.rs @@ -214,7 +214,7 @@ fn has_tolaria_vault_marker(path: &std::path::Path) -> bool { 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) + crate::git::init_repo(std::path::Path::new(vault_path.as_ref())) } #[cfg(desktop)] diff --git a/src-tauri/src/commands/vault/lifecycle_cmds.rs b/src-tauri/src/commands/vault/lifecycle_cmds.rs index ddedad7f..4125ce99 100644 --- a/src-tauri/src/commands/vault/lifecycle_cmds.rs +++ b/src-tauri/src/commands/vault/lifecycle_cmds.rs @@ -87,7 +87,7 @@ pub fn repair_vault(vault_path: String) -> Result { let vault_path = expand_tilde(&vault_path); vault::migrate_is_a_to_type(&vault_path)?; vault::repair_config_files(&vault_path)?; - git::ensure_gitignore(&vault_path)?; + git::ensure_gitignore(std::path::Path::new(vault_path.as_ref()))?; Ok("Vault repaired".to_string()) } diff --git a/src-tauri/src/git/mod.rs b/src-tauri/src/git/mod.rs index 5b1253f7..e7dc1f2a 100644 --- a/src-tauri/src/git/mod.rs +++ b/src-tauri/src/git/mod.rs @@ -57,13 +57,45 @@ const DEFAULT_GITIGNORE: &str = "# Tolaria app files (machine-specific, never co *.swo\n"; fn git_command() -> Command { - crate::hidden_command("git") + let mut command = crate::hidden_command("git"); + sanitize_linux_appimage_git_env(&mut command); + command +} + +#[cfg(any(test, all(desktop, target_os = "linux")))] +const LINUX_APPIMAGE_GIT_ENV_REMOVALS: [&str; 3] = + ["LD_LIBRARY_PATH", "LD_PRELOAD", "GIT_EXEC_PATH"]; + +#[cfg(all(desktop, target_os = "linux"))] +fn sanitize_linux_appimage_git_env(command: &mut Command) { + sanitize_linux_appimage_git_env_for_launch(command, linux_appimage_env_present()); +} + +#[cfg(not(all(desktop, target_os = "linux")))] +fn sanitize_linux_appimage_git_env(_command: &mut Command) {} + +#[cfg(any(test, all(desktop, target_os = "linux")))] +fn sanitize_linux_appimage_git_env_for_launch(command: &mut Command, is_appimage: bool) { + if !is_appimage { + return; + } + + for key in LINUX_APPIMAGE_GIT_ENV_REMOVALS { + command.env_remove(key); + } +} + +#[cfg(all(desktop, target_os = "linux"))] +fn linux_appimage_env_present() -> bool { + ["APPIMAGE", "APPDIR"] + .into_iter() + .any(|key| std::env::var(key).is_ok_and(|value| !value.trim().is_empty())) } /// Ensure a `.gitignore` with sensible defaults exists in the vault directory. /// Creates the file if missing; leaves existing `.gitignore` files untouched. -pub fn ensure_gitignore(path: &str) -> Result<(), String> { - let gitignore_path = Path::new(path).join(".gitignore"); +pub fn ensure_gitignore(path: impl AsRef) -> Result<(), String> { + let gitignore_path = path.as_ref().join(".gitignore"); if !gitignore_path.exists() { std::fs::write(&gitignore_path, DEFAULT_GITIGNORE) .map_err(|e| format!("Failed to write .gitignore: {}", e))?; @@ -72,15 +104,15 @@ pub fn ensure_gitignore(path: &str) -> Result<(), String> { } /// Initialize a new git repository, stage all files, and create an initial commit. -pub fn init_repo(path: &str) -> Result<(), String> { - let dir = Path::new(path); +pub fn init_repo(path: impl AsRef) -> Result<(), String> { + let dir = path.as_ref(); run_git(dir, &["init"])?; ensure_author_config(dir)?; // Write .gitignore before the first commit so machine-specific and // macOS metadata files are never tracked and don't cause conflicts. - ensure_gitignore(path)?; + ensure_gitignore(dir)?; run_git(dir, &["add", "."])?; commit_initial_vault_setup(dir)?; @@ -176,6 +208,7 @@ fn parse_github_repo_path(url: &str) -> Option { #[cfg(test)] mod tests { use super::*; + use std::collections::HashMap; use std::fs; use tempfile::TempDir; @@ -259,6 +292,18 @@ mod tests { (bare_dir, clone_a_dir, clone_b_dir) } + fn command_envs(command: &Command) -> HashMap> { + command + .get_envs() + .map(|(key, value)| { + ( + key.to_string_lossy().to_string(), + value.map(|entry| entry.to_string_lossy().to_string()), + ) + }) + .collect() + } + #[test] fn test_ensure_gitignore_creates_file() { let dir = TempDir::new().unwrap(); @@ -282,6 +327,32 @@ mod tests { assert_eq!(content, "my-rule\n"); } + #[test] + fn test_linux_appimage_git_commands_remove_appimage_loader_env() { + let mut command = crate::hidden_command("git"); + + sanitize_linux_appimage_git_env_for_launch(&mut command, true); + + let envs = command_envs(&command); + + for key in LINUX_APPIMAGE_GIT_ENV_REMOVALS { + assert_eq!(envs.get(key), Some(&None)); + } + } + + #[test] + fn test_non_appimage_git_commands_keep_parent_env_unmodified() { + let mut command = crate::hidden_command("git"); + + sanitize_linux_appimage_git_env_for_launch(&mut command, false); + + let envs = command_envs(&command); + + for key in LINUX_APPIMAGE_GIT_ENV_REMOVALS { + assert!(!envs.contains_key(key)); + } + } + #[test] fn test_init_repo_creates_git_directory() { let dir = TempDir::new().unwrap();