use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeIndexMismatch {
pub worktree_root: PathBuf,
pub index_root: PathBuf,
}
pub fn git_worktree_root(dir: &Path) -> Option<PathBuf> {
let output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.current_dir(dir)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let raw = String::from_utf8(output.stdout).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
realpath(Path::new(trimmed))
}
pub fn detect_worktree_index_mismatch(
start_path: &Path,
index_root: &Path,
) -> Option<WorktreeIndexMismatch> {
let worktree_root = git_worktree_root(start_path)?;
let resolved_index_root = realpath(index_root).unwrap_or_else(|| index_root.to_path_buf());
if worktree_root == resolved_index_root {
return None;
}
if git_worktree_root(&resolved_index_root)? != resolved_index_root {
return None;
}
Some(WorktreeIndexMismatch {
worktree_root,
index_root: resolved_index_root,
})
}
pub fn worktree_mismatch_warning(m: &WorktreeIndexMismatch) -> String {
format!(
"This tokensave index belongs to a different git working tree.\n \
Running in: {}\n \
Index from: {}\n\
Results reflect that tree's code (often a different branch), not this worktree — \
symbols changed only here are missing. Run `tokensave init` in this worktree for a \
worktree-local index.",
m.worktree_root.display(),
m.index_root.display()
)
}
pub fn worktree_mismatch_notice(m: &WorktreeIndexMismatch) -> String {
format!(
"WARNING: tokensave results below come from a different git worktree ({}), \
not where you're working ({}) — they may reflect another branch, and symbols \
changed only here are missing. Run `tokensave init` here for a worktree-local index.",
m.index_root.display(),
m.worktree_root.display()
)
}
fn realpath(p: &Path) -> Option<PathBuf> {
std::fs::canonicalize(p).ok()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::fs;
use std::process::Command;
use tempfile::tempdir;
fn run_git(cwd: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(cwd)
.status()
.expect("git not on PATH — required for worktree tests");
assert!(status.success(), "git {args:?} failed in {}", cwd.display());
}
#[test]
fn no_mismatch_outside_git() {
let tmp = tempdir().unwrap();
let index = tmp.path().join("index");
let start = tmp.path().join("start");
fs::create_dir_all(&index).unwrap();
fs::create_dir_all(&start).unwrap();
assert!(detect_worktree_index_mismatch(&start, &index).is_none());
}
#[test]
fn no_mismatch_when_index_lives_in_same_worktree() {
let tmp = tempdir().unwrap();
let project = tmp.path().join("repo");
fs::create_dir_all(&project).unwrap();
run_git(&project, &["init", "--quiet"]);
let sub = project.join("src");
fs::create_dir_all(&sub).unwrap();
assert!(detect_worktree_index_mismatch(&sub, &project).is_none());
}
#[test]
fn flags_mismatch_when_started_from_linked_worktree() {
let tmp = tempdir().unwrap();
let main = tmp.path().join("main");
fs::create_dir_all(&main).unwrap();
run_git(&main, &["init", "--quiet"]);
fs::write(main.join("README.md"), "hi").unwrap();
run_git(&main, &["add", "."]);
run_git(
&main,
&[
"-c",
"user.email=t@t",
"-c",
"user.name=t",
"commit",
"--quiet",
"-m",
"init",
],
);
let worktree = tmp.path().join("wt");
run_git(
&main,
&["worktree", "add", "--detach", worktree.to_str().unwrap()],
);
let mismatch = detect_worktree_index_mismatch(&worktree, &main)
.expect("expected mismatch when started from linked worktree but index is main");
assert_eq!(
mismatch.worktree_root,
std::fs::canonicalize(&worktree).unwrap()
);
assert_eq!(mismatch.index_root, std::fs::canonicalize(&main).unwrap());
}
#[test]
fn no_mismatch_when_index_root_is_plain_ancestor() {
let tmp = tempdir().unwrap();
let outer = tmp.path().join("outer"); let inner = outer.join("inner-repo");
fs::create_dir_all(&inner).unwrap();
run_git(&inner, &["init", "--quiet"]);
assert!(detect_worktree_index_mismatch(&inner, &outer).is_none());
}
}