Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a comment here that this is (probably) a temp workaround that should be dropped eventually?

}
RepoProvider::Jujutsu(repo) => {
let repo_path = repo.repo_path();
Expand Down
42 changes: 32 additions & 10 deletions src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -86,6 +78,13 @@ impl Session {
}
}

fn session_working_dir(repo: &RepoProvider) -> Result<PathBuf> {
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);
Expand Down Expand Up @@ -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());
}
}
121 changes: 121 additions & 0 deletions tests/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -121,3 +168,77 @@ fn find_repos_excludes_linked_worktree() {
repos.keys().collect::<Vec<_>>()
);
}

#[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"
);
}
Loading