diff --git a/src/repos.rs b/src/repos.rs index a796675..d1c3958 100644 --- a/src/repos.rs +++ b/src/repos.rs @@ -245,8 +245,12 @@ impl RepoProvider { pub fn is_worktree(&self) -> bool { match self { + // TODO: gix 0.85 misclassifies bare-repo worktrees as `Common` rather than `LinkedWorkTree`. + // We should revert our workaround once the upstream fix lands. + // https://github.com/GitoxideLabs/gitoxide/pull/2699 RepoProvider::Git(repo) => { matches!(repo.kind(), gix::repository::Kind::LinkedWorkTree) + || repo.workdir().is_some_and(|wd| wd.join(".git").is_file()) } RepoProvider::Jujutsu(repo) => { let repo_path = repo.repo_path(); diff --git a/src/session.rs b/src/session.rs index d9b8525..4b296b5 100644 --- a/src/session.rs +++ b/src/session.rs @@ -9,7 +9,7 @@ use crate::{ configs::{Config, VcsProviders}, dirty_paths::DirtyUtf8Path, error::TmsError, - repos::{find_repos, find_submodules, LazyRepoProvider}, + repos::{find_repos, find_submodules, LazyRepoProvider, RepoProvider}, tmux::Tmux, Result, }; @@ -50,15 +50,7 @@ impl Session { config: &Config, ) -> Result<()> { let repo = repo.resolve()?; - let path = if repo.is_bare() { - repo.path().to_path_buf().to_string()? - } else { - repo.work_dir() - .expect("bare repositories should all have parent directories") - .canonicalize() - .change_context(TmsError::IoError)? - .to_string()? - }; + let path = session_working_dir(repo)?.to_string()?; let session_name = self.name.replace('.', "_"); if !tmux.session_exists(&session_name) { @@ -86,6 +78,13 @@ impl Session { } } +fn session_working_dir(repo: &RepoProvider) -> Result { + match repo.work_dir() { + Some(work_dir) => work_dir.canonicalize().change_context(TmsError::IoError), + None => Ok(repo.path().to_path_buf()), + } +} + pub trait SessionContainer { fn find_session(&self, name: &str) -> Option<&Session>; fn insert_session(&mut self, name: String, repo: Session); @@ -266,4 +265,27 @@ mod tests { assert_eq!(deduplicated[1].name, "to/proj2/test"); assert_eq!(deduplicated[2].name, "to/proj1/test"); } + + #[test] + fn session_working_dir_of_bare_repo_worktree_is_the_checkout() { + use std::process::Command; + let dir = tempfile::tempdir().unwrap(); + let root = std::fs::canonicalize(dir.path()).unwrap(); + let bare = root.join("project"); + Command::new("git") + .args(["init", "--bare"]) + .arg(&bare) + .status() + .unwrap(); + Command::new("git") + .args(["worktree", "add", "master"]) + .current_dir(&bare) + .status() + .unwrap(); + + let worktree = LazyRepoProvider::new(&bare.join("master"), &[VcsProviders::Git]).unwrap(); + let dir = session_working_dir(worktree.resolve().unwrap()).unwrap(); + + assert_eq!(dir, std::fs::canonicalize(bare.join("master")).unwrap()); + } } diff --git a/tests/repos.rs b/tests/repos.rs index 7167fac..83ee76c 100644 --- a/tests/repos.rs +++ b/tests/repos.rs @@ -3,6 +3,7 @@ use std::{fs, path::PathBuf, process::Command}; use tempfile::tempdir; use tms::configs::{Config, SearchDirectory, VcsProviders}; use tms::repos::{find_repos, LazyRepoProvider}; +use tms::session::SessionType; fn config_searching(path: PathBuf, depth: usize) -> Config { Config { @@ -79,6 +80,52 @@ fn find_repos_includes_worktrees() { assert_eq!(repos.len(), 3); } +#[test] +fn find_repos_includes_worktrees_with_relative_paths() { + let dir = tempdir().unwrap(); + let search = dir.path().join("search"); + let project = search.join("my-project"); + fs::create_dir_all(&project).unwrap(); + let search = fs::canonicalize(&search).unwrap(); + Command::new("git") + .args(["init", "--bare"]) + .arg(&project) + .status() + .unwrap(); + Command::new("git") + .args(["config", "worktree.useRelativePaths", "true"]) + .current_dir(&project) + .status() + .unwrap(); + Command::new("git") + .args(["worktree", "add", "main"]) + .current_dir(&project) + .status() + .unwrap(); + Command::new("git") + .args(["worktree", "add", "test"]) + .current_dir(&project) + .status() + .unwrap(); + + // Ensure `worktree.useRelativePaths` took effect. + let gitdir = fs::read_to_string(project.join("worktrees/main/gitdir")).unwrap(); + assert!( + gitdir.trim_start().starts_with(".."), + "expected a relative gitdir, got: {gitdir:?}" + ); + + let mut config = config_searching(search, 2); + config.list_worktrees = Some(true); + + let repos = find_repos(&config).unwrap(); + + assert!(repos.contains_key("my-project")); + assert!(repos.contains_key("my-project#main")); + assert!(repos.contains_key("my-project#test")); + assert_eq!(repos.len(), 3); +} + #[test] fn find_repos_excludes_linked_worktree() { let dir = tempdir().unwrap(); @@ -121,3 +168,77 @@ fn find_repos_excludes_linked_worktree() { repos.keys().collect::>() ); } + +#[test] +fn find_repos_worktree_entry_is_a_worktree() { + let dir = tempdir().unwrap(); + let search = dir.path().join("search"); + let project = search.join("my-project"); + fs::create_dir_all(&project).unwrap(); + let search = fs::canonicalize(&search).unwrap(); + Command::new("git") + .args(["init", "--bare"]) + .arg(&project) + .status() + .unwrap(); + Command::new("git") + .args(["worktree", "add", "main"]) + .current_dir(&project) + .status() + .unwrap(); + Command::new("git") + .args(["worktree", "add", "test"]) + .current_dir(&project) + .status() + .unwrap(); + let mut config = config_searching(search, 2); + config.list_worktrees = Some(true); + + let repos = find_repos(&config).unwrap(); + + let main_entry = repos + .get("my-project#main") + .expect("expected a my-project#main entry"); + let SessionType::Git(main_worktree) = &main_entry[0].session_type else { + panic!("my-project#main should be a Git session"); + }; + assert!( + main_worktree.is_worktree().unwrap(), + "opening my-project#main should open only the `main` worktree, not fan out into siblings" + ); + + let repo_entry = repos + .get("my-project") + .expect("expected a my-project entry"); + let SessionType::Git(whole_repo) = &repo_entry[0].session_type else { + panic!("my-project should be a Git session"); + }; + assert!( + !whole_repo.is_worktree().unwrap(), + "the bare repo itself is not a worktree; opening it is what fans out into per-worktree windows" + ); +} + +#[test] +fn bare_repo_worktree_is_classified_as_worktree() { + let dir = tempdir().unwrap(); + let root = fs::canonicalize(dir.path()).unwrap(); + let bare = root.join("project"); + Command::new("git") + .args(["init", "--bare"]) + .arg(&bare) + .status() + .unwrap(); + Command::new("git") + .args(["worktree", "add", "master"]) + .current_dir(&bare) + .status() + .unwrap(); + + let worktree = LazyRepoProvider::new(&bare.join("master"), &[VcsProviders::Git]).unwrap(); + + assert!( + worktree.is_worktree().unwrap(), + "a worktree of a bare repo should be classified as a worktree" + ); +}