From 64e5729c19ce112dcad6a1a9788164681df920ae Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 19:20:48 -0800 Subject: [PATCH 1/4] chore: add shellexpand dependency for tilde expansion Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 12 +++++++++++- Cargo.toml | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index fe1478b..755898c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,7 +692,7 @@ dependencies = [ [[package]] name = "git-fs" -version = "0.1.1-alpha.1" +version = "0.1.2-alpha.1" dependencies = [ "async-trait", "base64", @@ -717,6 +717,7 @@ dependencies = [ "semver", "serde", "serde_json", + "shellexpand", "thiserror", "tokio", "toml", @@ -1889,6 +1890,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellexpand" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +dependencies = [ + "dirs", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 164756e..55faf7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ secrecy = { version = "0.10.3", features = ["serde"] } bimap = "0.6.3" self_update = { version = "0.42", default-features = false, features = ["rustls"] } semver = "1.0" +shellexpand = "3.1" inquire = "0.9.2" tracing-indicatif = "0.3.14" From eedcd5749d22e1152453cac5d1efad49cc135339 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 19:22:45 -0800 Subject: [PATCH 2/4] feat: add ExpandedPathBuf type with tilde expansion on deserialization Introduces a newtype wrapper around PathBuf that automatically expands ~ to the user's home directory during serde deserialization. All config path fields (mount_point, cache.path, daemon.pid_file) now use this type, ensuring tilde expansion happens at parse time. Co-Authored-By: Claude Opus 4.6 --- src/app_config.rs | 68 ++++++++++++++++++++++++++++++++++++++++------- src/daemon.rs | 2 +- src/main.rs | 2 +- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/app_config.rs b/src/app_config.rs index a87b03a..ccdd10a 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -16,6 +16,52 @@ use serde::{Deserialize, Serialize}; use crate::onboarding::{self, OnboardingError}; +/// A `PathBuf` that automatically expands `~` to the user's home directory +/// during deserialization. This ensures that any path loaded from configuration +/// is already resolved. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(transparent)] +pub struct ExpandedPathBuf(PathBuf); + +impl<'de> Deserialize<'de> for ExpandedPathBuf { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw = String::deserialize(deserializer)?; + let expanded = shellexpand::tilde(&raw); + Ok(Self(PathBuf::from(expanded.into_owned()))) + } +} + +impl ExpandedPathBuf { + /// Creates a new `ExpandedPathBuf` from any path, without expansion. + /// Use this for programmatically-constructed paths that are already absolute. + pub fn new(path: PathBuf) -> Self { + Self(path) + } +} + +impl std::ops::Deref for ExpandedPathBuf { + type Target = Path; + + fn deref(&self) -> &Path { + &self.0 + } +} + +impl AsRef for ExpandedPathBuf { + fn as_ref(&self) -> &Path { + &self.0 + } +} + +impl std::fmt::Display for ExpandedPathBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.display().fmt(f) + } +} + fn mesa_runtime_dir() -> Option { let runtime_dir = dirs::runtime_dir(); if let Some(path) = runtime_dir { @@ -30,15 +76,17 @@ fn mesa_runtime_dir() -> Option { None } -fn default_pid_file() -> PathBuf { - mesa_runtime_dir().map_or_else( +fn default_pid_file() -> ExpandedPathBuf { + ExpandedPathBuf::new(mesa_runtime_dir().map_or_else( || PathBuf::from("/var/run/git-fs.pid"), |rd| rd.join("git-fs.pid"), - ) + )) } -fn default_mount_point() -> PathBuf { - mesa_runtime_dir().map_or_else(|| PathBuf::from("/tmp/git-fs/mnt"), |rd| rd.join("mnt")) +fn default_mount_point() -> ExpandedPathBuf { + ExpandedPathBuf::new( + mesa_runtime_dir().map_or_else(|| PathBuf::from("/tmp/git-fs/mnt"), |rd| rd.join("mnt")), + ) } fn current_uid() -> u32 { @@ -57,15 +105,15 @@ pub struct CacheConfig { pub max_size: Option, /// The path to the cache directory. - pub path: PathBuf, + pub path: ExpandedPathBuf, } impl Default for CacheConfig { fn default() -> Self { Self { max_size: None, - path: mesa_runtime_dir() - .map_or_else(|| PathBuf::from("/tmp/git-fs/cache"), |rd| rd.join("cache")), + path: ExpandedPathBuf::new(mesa_runtime_dir() + .map_or_else(|| PathBuf::from("/tmp/git-fs/cache"), |rd| rd.join("cache"))), } } } @@ -184,7 +232,7 @@ impl<'a> From<&'a OrganizationConfig> for DangerousOrganizationConfig<'a> { pub struct DaemonConfig { /// The path to the PID file for the daemon. Uses /var/run/git-fs.pid if not specified. #[serde(default = "default_pid_file")] - pub pid_file: PathBuf, + pub pid_file: ExpandedPathBuf, } impl Default for DaemonConfig { @@ -214,7 +262,7 @@ pub struct Config { /// The mount point for the filesystem. #[serde(default = "default_mount_point")] - pub mount_point: PathBuf, + pub mount_point: ExpandedPathBuf, /// The user to mount the filesystem as. If not specified, runs as the current user. #[serde(default = "current_uid")] diff --git a/src/daemon.rs b/src/daemon.rs index e56e708..4e68bea 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -66,7 +66,7 @@ mod managed_fuse { impl ManagedFuse { pub fn new(config: &app_config::Config) -> Self { Self { - mount_point: config.mount_point.clone(), + mount_point: config.mount_point.to_path_buf(), } } diff --git a/src/main.rs b/src/main.rs index e2321c0..5ddb0b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,7 +94,7 @@ fn main() { } let daemonize = daemonize::Daemonize::new() - .pid_file(config.daemon.pid_file.clone()) + .pid_file(&config.daemon.pid_file) .chown_pid_file(true) .user(config.uid) .group(config.gid); From ba6283dc8c86e65d2ce77a6ab49bd03d61ae4545 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 19:23:07 -0800 Subject: [PATCH 3/4] feat: expand tilde in onboarding wizard mount point input Co-Authored-By: Claude Opus 4.6 --- src/onboarding.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/onboarding.rs b/src/onboarding.rs index 2dbfac3..3fdcb02 100644 --- a/src/onboarding.rs +++ b/src/onboarding.rs @@ -1,11 +1,12 @@ //! Interactive onboarding wizard for first-time configuration. -use std::{io::IsTerminal as _, path::PathBuf}; +use std::io::IsTerminal as _; +use std::path::PathBuf; use inquire::{Confirm, Password, Text, validator::Validation}; use secrecy::SecretString; -use crate::app_config::{Config, OrganizationConfig}; +use crate::app_config::{Config, ExpandedPathBuf, OrganizationConfig}; const WELCOME_MESSAGE: &str = " \x1b[32m@@@@\x1b[0m Welcome to \x1b[1mgit-fs\x1b[0m! Let's get you started! @@ -49,7 +50,9 @@ pub fn run_wizard() -> Result { let mount_point_str = Text::new("Where should git-fs mount the filesystem?") .with_default(&defaults.mount_point.display().to_string()) .prompt()?; - let mount_point = PathBuf::from(mount_point_str); + let mount_point = ExpandedPathBuf::new(PathBuf::from( + shellexpand::tilde(&mount_point_str).into_owned(), + )); let mut org_keys: Vec<(String, SecretString)> = Vec::new(); loop { From 364cfb2faf64a6a5363f35764b0704d2eea25287 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 19:29:26 -0800 Subject: [PATCH 4/4] cargo fmt --- src/app_config.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app_config.rs b/src/app_config.rs index ccdd10a..79b43f7 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -112,8 +112,10 @@ impl Default for CacheConfig { fn default() -> Self { Self { max_size: None, - path: ExpandedPathBuf::new(mesa_runtime_dir() - .map_or_else(|| PathBuf::from("/tmp/git-fs/cache"), |rd| rd.join("cache"))), + path: ExpandedPathBuf::new( + mesa_runtime_dir() + .map_or_else(|| PathBuf::from("/tmp/git-fs/cache"), |rd| rd.join("cache")), + ), } } }