diff options
| author | Paul Hinze <phinze@phinze.com> | 2026-03-30 17:49:59 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-30 23:49:59 +0100 |
| commit | dcfbe9fd2c8d248eb9edecd0be1f6a8a35e2d1de (patch) | |
| tree | fe744796fcc6e98a676cdabb0f2ca8ffa6a79cf7 /crates/atuin-common/src/utils.rs | |
| parent | chore: Update to eye-declare 0.3.0 (#3365) (diff) | |
| download | atuin-dcfbe9fd2c8d248eb9edecd0be1f6a8a35e2d1de.zip | |
fix: resolve git worktrees to main repo in workspace filter (#3366)
Fixes #3364
## Summary
When using `filter_mode = "workspace"`, each git worktree gets its own
isolated history scope instead of sharing history with the main
checkout.
## Root cause
`in_git_repo()` walks up the directory tree looking for a `.git` entry.
In a worktree, `.git` is a file (not a directory) containing a `gitdir:`
pointer back to the main repo's `.git/worktrees/<name>`. Since
`has_git_dir()` just checks `.exists()`, the worktree's own path becomes
the workspace root, and the `WHERE cwd LIKE '<root>%'` filter isolates
its history from the main repo and other worktrees.
## Fix
Add `resolve_git_worktree()`, which reads the `.git` file when it's not
a directory, parses the `gitdir:` pointer, and walks back up to find the
parent containing a real `.git` directory. No new dependencies -- just a
bit of file reading and path traversal.
## Testing
Two new tests in `utils::tests`:
- `in_git_repo_regular` -- baseline that normal repos still resolve
correctly
- `in_git_repo_worktree_resolves_to_main_repo` -- creates a simulated
worktree layout and verifies it resolves to the main repo root
Both pass locally, along with the full `atuin-common` test suite and
`cargo clippy -- -D warnings`.
---
Disclosure: I'm a Rust novice, and I put this together with help from
Claude Code. I'm eager to learn more, so please let me know if anything
doesn't feel like idiomatic Rust!
## Checks
- [x] I am happy for maintainers to push small adjustments to this PR,
to speed up the review cycle
- [x] I have checked that there are no existing pull requests for the
same thing
Diffstat (limited to '')
| -rw-r--r-- | crates/atuin-common/src/utils.rs | 84 |
1 files changed, 83 insertions, 1 deletions
diff --git a/crates/atuin-common/src/utils.rs b/crates/atuin-common/src/utils.rs index b885423e..1a6fb8b7 100644 --- a/crates/atuin-common/src/utils.rs +++ b/crates/atuin-common/src/utils.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; use std::env; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use eyre::{Result, eyre}; @@ -43,6 +43,38 @@ pub fn has_git_dir(path: &str) -> bool { gitdir.exists() } +// in a git worktree, .git is a file containing "gitdir: <path>" pointing +// to the main repo's .git/worktrees/<name> directory. follow the pointer +// back to the main repo root so all worktrees share a workspace. +fn resolve_git_worktree(path: &Path) -> Option<PathBuf> { + let git_path = path.join(".git"); + + if !git_path.is_file() { + return None; + } + + let contents = std::fs::read_to_string(&git_path).ok()?; + let gitdir_str = contents.strip_prefix("gitdir: ")?.trim(); + + let gitdir = PathBuf::from(gitdir_str); + let gitdir = if gitdir.is_absolute() { + gitdir + } else { + path.join(gitdir_str) + }; + + // walk up from e.g. /repo/.git/worktrees/feature to find /repo + let mut candidate = gitdir.as_path(); + while let Some(parent) = candidate.parent() { + if parent.join(".git").is_dir() { + return Some(parent.to_path_buf()); + } + candidate = parent; + } + + None +} + // detect if any parent dir has a git repo in it // I really don't want to bring in libgit for something simple like this // If we start to do anything more advanced, then perhaps @@ -55,6 +87,10 @@ pub fn in_git_repo(path: &str) -> Option<PathBuf> { // No parent? then we hit root, finding no git if gitdir.parent().is_some() { + // if .git is a file (worktree), resolve to the main repo root + if let Some(main_repo) = resolve_git_worktree(&gitdir) { + return Some(main_repo); + } return Some(gitdir); } @@ -286,6 +322,52 @@ mod tests { )); } + #[cfg(not(windows))] + #[test] + fn in_git_repo_regular() { + // regular git repo should resolve to the directory containing .git + let tmp = std::env::temp_dir().join("atuin-test-regular-git"); + let _ = std::fs::remove_dir_all(&tmp); + let subdir = tmp.join("src").join("deep"); + std::fs::create_dir_all(&subdir).unwrap(); + std::fs::create_dir_all(tmp.join(".git")).unwrap(); + + let result = in_git_repo(subdir.to_str().unwrap()); + assert_eq!(result, Some(tmp.clone())); + + std::fs::remove_dir_all(&tmp).unwrap(); + } + + #[cfg(not(windows))] + #[test] + fn in_git_repo_worktree_resolves_to_main_repo() { + // worktree .git is a file pointing back to the main repo — + // in_git_repo should follow it so all worktrees share a workspace + let tmp = std::env::temp_dir().join("atuin-test-worktree-git"); + let _ = std::fs::remove_dir_all(&tmp); + + // main repo at tmp/main with a real .git directory + let main_repo = tmp.join("main"); + let worktree_git_dir = main_repo.join(".git").join("worktrees").join("feature"); + std::fs::create_dir_all(&worktree_git_dir).unwrap(); + + // worktree at tmp/worktree with a .git file + let worktree = tmp.join("worktree"); + let worktree_subdir = worktree.join("src"); + std::fs::create_dir_all(&worktree_subdir).unwrap(); + std::fs::write( + worktree.join(".git"), + format!("gitdir: {}", worktree_git_dir.to_str().unwrap()), + ) + .unwrap(); + + // should resolve to the main repo root, not the worktree root + let result = in_git_repo(worktree_subdir.to_str().unwrap()); + assert_eq!(result, Some(main_repo.clone())); + + std::fs::remove_dir_all(&tmp).unwrap(); + } + #[test] fn dumb_random_test() { // Obviously not a test of randomness, but make sure we haven't made some |
