use std::{
collections::HashMap,
path::{Path, PathBuf},
process::Command,
sync::{LazyLock, Mutex},
};
static SCOPE_CACHE: LazyLock<Mutex<HashMap<PathBuf, PathBuf>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) fn resolve_session_scope_path(cwd: &Path) -> PathBuf {
let mut cache = SCOPE_CACHE.lock().expect("session scope cache poisoned");
resolve_session_scope_path_with_cache(cwd, &mut cache, &mut run_git_command)
}
fn resolve_session_scope_path_with_cache(
cwd: &Path,
cache: &mut HashMap<PathBuf, PathBuf>,
run_git: &mut impl FnMut(&Path, &[&str]) -> Option<GitCommandResult>,
) -> PathBuf {
if let Some(path) = cache.get(cwd) {
return path.clone();
}
let path = discover_session_scope_path(cwd, run_git);
cache.insert(cwd.to_path_buf(), path.clone());
path
}
fn discover_session_scope_path(
cwd: &Path,
run_git: &mut impl FnMut(&Path, &[&str]) -> Option<GitCommandResult>,
) -> PathBuf {
if is_inside_git_worktree(cwd, run_git)
&& let Some(path) = primary_worktree_path(cwd, run_git)
{
return canonicalize_or_original(path);
}
cwd.to_path_buf()
}
fn run_git_command(cwd: &Path, args: &[&str]) -> Option<GitCommandResult> {
let output = Command::new("git")
.args(["-C"])
.arg(cwd)
.args(args)
.output()
.ok()?;
Some(GitCommandResult {
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
})
}
struct GitCommandResult {
success: bool,
stdout: String,
}
fn is_inside_git_worktree(
cwd: &Path,
run_git: &mut impl FnMut(&Path, &[&str]) -> Option<GitCommandResult>,
) -> bool {
let Some(output) = run_git(cwd, &["rev-parse", "--is-inside-work-tree"]) else {
return false;
};
output.success && output.stdout.trim() == "true"
}
fn primary_worktree_path(
cwd: &Path,
run_git: &mut impl FnMut(&Path, &[&str]) -> Option<GitCommandResult>,
) -> Option<PathBuf> {
let output = run_git(cwd, &["worktree", "list", "--porcelain"])?;
if !output.success {
return None;
}
output
.stdout
.lines()
.find_map(|line| line.strip_prefix("worktree ").map(PathBuf::from))
}
fn canonicalize_or_original(path: PathBuf) -> PathBuf {
path.canonicalize().unwrap_or(path)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use tempfile::TempDir;
fn result(success: bool, stdout: &str) -> GitCommandResult {
GitCommandResult {
success,
stdout: stdout.to_owned(),
}
}
#[test]
fn non_git_directory_returns_literal_cwd() {
let temp = TempDir::new().unwrap();
let cwd = temp.path().join("plain");
std::fs::create_dir(&cwd).unwrap();
let mut cache = HashMap::new();
let mut run_git = |_: &Path, _: &[&str]| None;
assert_eq!(
resolve_session_scope_path_with_cache(&cwd, &mut cache, &mut run_git),
cwd
);
}
#[test]
fn git_command_failure_returns_cwd() {
let temp = TempDir::new().unwrap();
let cwd = temp.path().join("missing");
let mut cache = HashMap::new();
let mut run_git = |_: &Path, _: &[&str]| Some(result(false, ""));
assert_eq!(
resolve_session_scope_path_with_cache(&cwd, &mut cache, &mut run_git),
cwd
);
}
#[test]
fn primary_worktree_is_canonicalized() {
let temp = TempDir::new().unwrap();
let cwd = temp.path().join("linked");
let primary = temp.path().join("repo");
std::fs::create_dir(&cwd).unwrap();
std::fs::create_dir(&primary).unwrap();
let mut cache = HashMap::new();
let mut run_git = |_: &Path, args: &[&str]| {
Some(if args[0] == "rev-parse" {
result(true, "true\n")
} else {
result(true, &format!("worktree {}\n", primary.display()))
})
};
assert_eq!(
resolve_session_scope_path_with_cache(&cwd, &mut cache, &mut run_git),
primary.canonicalize().unwrap()
);
}
#[test]
fn linked_worktree_fixture_preserves_primary_path() {
let temp = TempDir::new().unwrap();
let cwd = temp.path().join("linked");
let primary = temp.path().join("repo");
std::fs::create_dir(&cwd).unwrap();
let mut cache = HashMap::new();
let mut run_git = |_: &Path, args: &[&str]| {
Some(if args[0] == "rev-parse" {
result(true, "true\n")
} else {
result(
true,
&format!(
"worktree {}\nworktree {}\n",
primary.display(),
cwd.display()
),
)
})
};
assert_eq!(
resolve_session_scope_path_with_cache(&cwd, &mut cache, &mut run_git),
primary
);
}
#[test]
fn missing_primary_path_uses_literal_path() {
let temp = TempDir::new().unwrap();
let cwd = temp.path().join("linked");
let primary = temp.path().join("missing-primary");
let mut cache = HashMap::new();
let mut run_git = |_: &Path, args: &[&str]| {
Some(if args[0] == "rev-parse" {
result(true, "true")
} else {
result(true, &format!("worktree {}\n", primary.display()))
})
};
assert_eq!(
resolve_session_scope_path_with_cache(&cwd, &mut cache, &mut run_git),
primary
);
}
#[test]
fn repeated_resolution_uses_cached_result() {
let temp = TempDir::new().unwrap();
let cwd = temp.path().join("repo");
let primary = temp.path().join("primary");
let calls = Cell::new(0);
let mut cache = HashMap::new();
let mut run_git = |_: &Path, args: &[&str]| {
calls.set(calls.get() + 1);
Some(if args[0] == "rev-parse" {
result(true, "true")
} else {
result(true, &format!("worktree {}\n", primary.display()))
})
};
let first = resolve_session_scope_path_with_cache(&cwd, &mut cache, &mut run_git);
let second = resolve_session_scope_path_with_cache(&cwd, &mut cache, &mut run_git);
assert_eq!(first, second);
assert_eq!(calls.get(), 2);
}
}