fix: sanitize appimage git subprocess env

This commit is contained in:
lucaronin
2026-05-01 02:01:49 +02:00
parent 0987946cff
commit 568ada8992
6 changed files with 85 additions and 10 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
```

View File

@@ -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)]

View File

@@ -87,7 +87,7 @@ pub fn repair_vault(vault_path: String) -> Result<String, String> {
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())
}

View File

@@ -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<Path>) -> 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<Path>) -> 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<String> {
#[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<String, Option<String>> {
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();