diff --git a/cli/tests/test_git_colocated.rs b/cli/tests/test_git_colocated.rs index e3b607c49..f0629dac0 100644 --- a/cli/tests/test_git_colocated.rs +++ b/cli/tests/test_git_colocated.rs @@ -975,3 +975,354 @@ fn stopgap_workspace_colocate( .assert() .success(); } + +#[test] +fn test_colocated_workspace_in_bare_repo() { + // TODO: Remove when this stops requiring git (stopgap_workspace_colocate) + if Command::new("git").arg("--version").status().is_err() { + eprintln!("Skipping because git command might fail to run"); + return; + } + + let test_env = TestEnvironment::default(); + let repo_path = test_env.env_root().join("repo"); + let second_path = test_env.env_root().join("second"); + // + // git init without --colocate creates a bare repo + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + std::fs::write(repo_path.join("file"), "contents").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "initial commit"]); + let (initial_commit, _) = test_env.jj_cmd_ok( + &repo_path, + &["log", "--no-graph", "-T", "commit_id", "-r", "@-"], + ); + // TODO: replace with workspace add, when it can create worktrees + stopgap_workspace_colocate(&test_env, &repo_path, false, "../second", &initial_commit); + + insta::assert_snapshot!(get_log_output(&test_env, &second_path), @r#" + @ baf7f13355a30ddd3aa6476317fcbc9c65239b0c second@ + │ ○ 45c9d8477181a2b9c077ff1b724694fe0969b301 default@ + ├─╯ + ○ 046d74c8ab0a4730e58488508a5398b7a91e54a2 git_head() initial commit + ◆ 0000000000000000000000000000000000000000 + "#); + + test_env.jj_cmd_ok( + &second_path, + &["commit", "-m", "commit in second workspace"], + ); + insta::assert_snapshot!(get_log_output(&test_env, &second_path), @r#" + @ fca81879c29229d0097cb7d32fc8a661ee80c6e4 second@ + ○ 220827d1ceb632ec7dd4cb2f5110b496977d14c2 git_head() commit in second workspace + │ ○ 45c9d8477181a2b9c077ff1b724694fe0969b301 default@ + ├─╯ + ○ 046d74c8ab0a4730e58488508a5398b7a91e54a2 initial commit + ◆ 0000000000000000000000000000000000000000 + "#); + + // FIXME: There should still be no git HEAD in the default workspace, which + // is not colocated. However, git_head() is a property of the view. And + // currently, all colocated workspaces read and write from the same + // entry of the common view. + // + // let stdout = test_env.jj_cmd_success(&repo_path, &["log", "--no-graph", + // "-r", "git_head()"]); insta::assert_snapshot!(stdout, @r#""#); + + let stdout = test_env.jj_cmd_success( + &second_path, + &["op", "log", "-Tself.description().first_line()"], + ); + insta::assert_snapshot!(stdout, @r#" + @ commit baf7f13355a30ddd3aa6476317fcbc9c65239b0c + ○ import git head + ○ create initial working-copy commit in workspace second + ○ add workspace 'second' + ○ commit 4e8f9d2be039994f589b4e57ac5e9488703e604d + ○ snapshot working copy + ○ add workspace 'default' + ○ + "#); +} + +#[test] +fn test_colocated_workspace_moved_original_on_disk() { + if Command::new("git").arg("--version").status().is_err() { + eprintln!("Skipping because git command might fail to run"); + return; + } + + let test_env = TestEnvironment::default(); + let repo_path = test_env.env_root().join("repo"); + let second_path = test_env.env_root().join("second"); + let new_repo_path = test_env.env_root().join("repo-moved"); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]); + std::fs::write(repo_path.join("file"), "contents").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "initial commit"]); + let (initial_commit, _) = test_env.jj_cmd_ok( + &repo_path, + &["log", "--no-graph", "-T", "commit_id", "-r", "@-"], + ); + // TODO: replace with workspace add, when it can create worktrees + stopgap_workspace_colocate(&test_env, &repo_path, true, "../second", &initial_commit); + + // Break our worktree by moving the original repo on disk + std::fs::rename(&repo_path, &new_repo_path).unwrap(); + // imagine JJ were able to do this + std::fs::write( + second_path.join(".jj/repo"), + new_repo_path + .join(".jj/repo") + .as_os_str() + .as_encoded_bytes(), + ) + .unwrap(); + + let (_, stderr) = test_env.jj_cmd_ok(&second_path, &["status"]); + // hack for windows paths + let gitfile_contents = std::fs::read_to_string(second_path.join(".git")) + .unwrap() + .strip_prefix("gitdir: ") + .unwrap() + .trim() + .to_owned(); + let stderr = stderr.replace(&gitfile_contents, "$TEST_ENV/repo/.git/worktrees/second"); + insta::assert_snapshot!(stderr, @r" + Warning: Workspace is a broken Git worktree + The .git file points at: $TEST_ENV/repo/.git/worktrees/second + Hint: If this is meant to be a colocated JJ workspace, you may like to try `git -C $TEST_ENV/repo-moved worktree repair` + "); + + Command::new("git") + .args(["worktree", "repair"]) + .current_dir(&new_repo_path) + .assert() + .success(); + insta::assert_snapshot!(get_log_output(&test_env, &second_path), @r#" + @ 05530a3e0f9d581260343e273d66c381e76957df second@ + │ ○ 45c9d8477181a2b9c077ff1b724694fe0969b301 default@ + ├─╯ + ○ 046d74c8ab0a4730e58488508a5398b7a91e54a2 git_head() initial commit + ◆ 0000000000000000000000000000000000000000 + "#); +} + +#[test] +fn test_colocated_workspace_wrong_gitdir() { + // TODO: Remove when this stops requiring git (stopgap_workspace_colocate) + if Command::new("git").arg("--version").status().is_err() { + eprintln!("Skipping because git command might fail to run"); + return; + } + + let test_env = TestEnvironment::default(); + let repo_path = test_env.env_root().join("repo"); + let second_path = test_env.env_root().join("second"); + let other_path = test_env.env_root().join("other"); + let other_second_path = test_env.env_root().join("other_second"); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]); + std::fs::write(repo_path.join("file"), "contents").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "initial commit"]); + let (initial_commit, _) = test_env.jj_cmd_ok( + &repo_path, + &["log", "--no-graph", "-T", "commit_id", "-r", "@-"], + ); + // TODO: replace with workspace add, when it can create worktrees + stopgap_workspace_colocate(&test_env, &repo_path, true, "../second", &initial_commit); + + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "other"]); + std::fs::write(other_path.join("file"), "contents2").unwrap(); + test_env.jj_cmd_ok(&other_path, &["commit", "-m", "initial commit"]); + let (ic_other, _) = test_env.jj_cmd_ok( + &other_path, + &["log", "--no-graph", "-T", "commit_id", "-r", "@-"], + ); + // TODO: replace with workspace add, when it can create worktrees + stopgap_workspace_colocate(&test_env, &other_path, true, "../other_second", &ic_other); + + // Break one of our worktrees + std::fs::copy(other_second_path.join(".git"), second_path.join(".git")).unwrap(); + + let (_, stderr) = test_env.jj_cmd_ok(&second_path, &["status"]); + insta::assert_snapshot!(stderr, @"Warning: Workspace is also a Git worktree that is not managed by JJ"); +} + +#[test] +fn test_colocated_workspace_invalid_gitdir() { + // TODO: Remove when this stops requiring git (stopgap_workspace_colocate) + if Command::new("git").arg("--version").status().is_err() { + eprintln!("Skipping because git command might fail to run"); + return; + } + + let test_env = TestEnvironment::default(); + let repo_path = test_env.env_root().join("repo"); + let second_path = test_env.env_root().join("second"); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]); + std::fs::write(repo_path.join("file"), "contents").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "initial commit"]); + let (initial_commit, _) = test_env.jj_cmd_ok( + &repo_path, + &["log", "--no-graph", "-T", "commit_id", "-r", "@-"], + ); + // TODO: replace with workspace add, when it can create worktrees + stopgap_workspace_colocate(&test_env, &repo_path, true, "../second", &initial_commit); + + // Break one of our worktrees + std::fs::write(second_path.join(".git"), "invalid").unwrap(); + + let (_, stderr) = test_env.jj_cmd_ok(&second_path, &["status"]); + insta::assert_snapshot!(stderr, @r" + Warning: Workspace is a broken Git worktree + The .git file points at: invalid + Hint: If this is meant to be a colocated JJ workspace, you may like to try `git -C $TEST_ENV/repo worktree repair` + "); +} + +#[test] +fn test_colocated_workspace_independent_heads() { + // TODO: Remove when this stops requiring git (stopgap_workspace_colocate) + if Command::new("git").arg("--version").status().is_err() { + eprintln!("Skipping because git command might fail to run"); + return; + } + + let test_env = TestEnvironment::default(); + let repo_path = test_env.env_root().join("repo"); + let second_path = test_env.env_root().join("second"); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]); + // create a commit so that git can have a HEAD + std::fs::write(repo_path.join("file"), "contents").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "initial commit"]); + let (initial_commit, _) = test_env.jj_cmd_ok( + &repo_path, + &["log", "--no-graph", "-T", "commit_id", "-r", "@-"], + ); + // TODO: replace with workspace add, when it can create worktrees + stopgap_workspace_colocate(&test_env, &repo_path, true, "../second", &initial_commit); + + { + let first_git = git2::Repository::open(&repo_path).unwrap(); + assert!(first_git.head_detached().unwrap()); + let first_head = first_git.head().unwrap(); + + let commit = first_head.peel_to_commit().unwrap().id(); + assert_eq!(commit.to_string(), initial_commit); + + let second_git = git2::Repository::open(&second_path).unwrap(); + assert!(second_git.head_detached().unwrap()); + let second_head = second_git.head().unwrap(); + + let commit = second_head.peel_to_commit().unwrap().id(); + assert_eq!(commit.to_string(), initial_commit); + } + + // now commit again in the second worktree, and make sure the original + // repo's head does not move. + // + // This tests that we are writing HEAD to the corresponding worktree, + // rather than unconditionally to the default workspace. + std::fs::write(repo_path.join("file2"), "contents").unwrap(); + test_env.jj_cmd_ok(&second_path, &["commit", "-m", "followup commit"]); + let (followup_commit, _) = test_env.jj_cmd_ok( + &second_path, + &["log", "--no-graph", "-T", "commit_id", "-r", "@-"], + ); + + { + // git HEAD should not move in the default workspace + let first_git = git2::Repository::open(&repo_path).unwrap(); + assert!(first_git.head_detached().unwrap()); + let first_head = first_git.head().unwrap(); + // still initial + assert_eq!( + first_head.peel_to_commit().unwrap().id().to_string(), + initial_commit, + "default workspace's git HEAD should not have moved from {initial_commit}" + ); + + let second_git = git2::Repository::open(&second_path).unwrap(); + assert!(second_git.head_detached().unwrap()); + let second_head = second_git.head().unwrap(); + assert_eq!( + second_head.peel_to_commit().unwrap().id().to_string(), + followup_commit, + "second workspace's git HEAD should have advanced to {followup_commit}" + ); + } + + // Finally, test imports. Test that a commit written to HEAD in one workspace + // does not get imported by the other workspace. + + // Write in default, expect second not to import it + let new_commit = test_independent_import(&test_env, &repo_path, &second_path, &followup_commit); + // Write in second, expect default not to import it + test_independent_import(&test_env, &second_path, &repo_path, &new_commit); + + fn test_independent_import( + test_env: &TestEnvironment, + commit_in: &Path, + no_import_in_workspace: &Path, + workspace_at: &str, + ) -> String { + // Commit in one workspace + let mut repo = gix::open(commit_in).unwrap(); + { + use gix::config::tree::*; + let mut config = repo.config_snapshot_mut(); + let (name, email) = ("JJ test", "jj@example.com"); + config.set_value(&Author::NAME, name).unwrap(); + config.set_value(&Author::EMAIL, email).unwrap(); + config.set_value(&Committer::NAME, name).unwrap(); + config.set_value(&Committer::EMAIL, email).unwrap(); + } + let tree = repo.head_tree_id().unwrap(); + let current = repo.head_commit().unwrap().id; + let new_commit = repo + .commit( + "HEAD", + format!("empty commit in {}", commit_in.display()), + tree, + [current], + ) + .unwrap() + .to_string(); + + let (check_git_head, stderr) = test_env.jj_cmd_ok( + no_import_in_workspace, + &["log", "--no-graph", "-r", "git_head()", "-T", "commit_id"], + ); + // Asserting stderr is empty => no import occurred + assert_eq!( + stderr, + "", + "Should not have imported HEAD in workspace {}", + no_import_in_workspace.display() + ); + // And the commit_id should be pointing to what it was before + assert_eq!( + check_git_head, + workspace_at, + "should still be at {workspace_at} in workspace {}", + no_import_in_workspace.display() + ); + + // Now we import the new HEAD in the commit_in workspace, so it's up to date. + let (check_git_head, stderr) = test_env.jj_cmd_ok( + commit_in, + &["log", "--no-graph", "-r", "git_head()", "-T", "commit_id"], + ); + assert_eq!( + stderr, + "Reset the working copy parent to the new Git HEAD.\n", + "should have imported HEAD in workspace {}", + commit_in.display() + ); + assert_eq!( + check_git_head, + new_commit, + "should have advanced to {new_commit} in workspace {}", + commit_in.display() + ); + new_commit + } +} diff --git a/cli/tests/test_git_init.rs b/cli/tests/test_git_init.rs index f9e0da59a..8fd5decf6 100644 --- a/cli/tests/test_git_init.rs +++ b/cli/tests/test_git_init.rs @@ -731,6 +731,237 @@ fn test_git_init_external_but_git_dir_exists() { "###); } +fn create_commit(git_repo: &git2::Repository, msg: &str, parents: &[&git2::Commit]) -> git2::Oid { + let author = git2::Signature::new("JJ test", "jj@example.com", &git2::Time::new(0, 0)).unwrap(); + let empty_tree = git_repo.treebuilder(None).unwrap().write().unwrap(); + let oid = git_repo + .commit( + Some("HEAD"), + &author, + &author, + msg, + &git_repo.find_tree(empty_tree).unwrap(), + parents, + ) + .unwrap(); + oid +} + +#[test] +fn test_git_init_external_pointing_at_worktree_from_outside() { + let test_env = TestEnvironment::default(); + let git_repo_path = test_env.env_root().join("git-repo"); + let worktree_path = test_env.env_root().join("worktree"); + let workspace_root = test_env.env_root().join("repo"); + let git_repo = git2::Repository::init(&git_repo_path).unwrap(); + // Must create a commit so we can create a worktree + let initial_commit = create_commit(&git_repo, "initial commit", &[]); + let _worktree = git_repo + .worktree("jj-worktree", &worktree_path, None) + .unwrap(); + + // now commit in the worktree, so we know where we are importing from + let worktree_repo = git2::Repository::open(&worktree_path).unwrap(); + let _initial_commit = create_commit( + &worktree_repo, + "second commit", + &[&worktree_repo.find_commit(initial_commit).unwrap()], + ) + .to_string(); + + std::fs::create_dir(&workspace_root).unwrap(); + let (stdout, stderr) = test_env.jj_cmd_ok( + &workspace_root, + &["git", "init", "--git-repo", worktree_path.to_str().unwrap()], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Done importing changes from the underlying Git repo. + Working copy now at: sqpuoqvx 58a55009 (empty) (no description set) + Parent commit : kyuukqus 49c63010 jj-worktree | (empty) second commit + Initialized repo in "." + "#); + + assert_eq!( + PathBuf::from(read_git_target(&workspace_root)) + .canonicalize() + .unwrap(), + worktree_path.join(".git") + ); + + // This is similar to a normal `jj git init --git-repo=` -- we import the + // commits, but in this case our HEAD@git comes from the worktree. + let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root); + insta::assert_snapshot!(stdout, @r#" + @ 58a55009f1c1 + ○ 49c630103a76 jj-worktree git_head() second commit + ○ 70618e4d103f master initial commit + ◆ 000000000000 + "#); + insta::assert_snapshot!(stderr, @""); + + // The git HEAD should not advance, because this is not colocated + test_env.jj_cmd_ok(&workspace_root, &["new"]); + let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root); + insta::assert_snapshot!(stdout, @r#" + @ f133c02dde73 + ○ 58a55009f1c1 + ○ 49c630103a76 jj-worktree git_head() second commit + ○ 70618e4d103f master initial commit + ◆ 000000000000 + "#); + insta::assert_snapshot!(stderr, @""); +} + +#[test] +fn test_git_init_external_in_worktree_pointing_worktree() { + let test_env = TestEnvironment::default(); + let git_repo_path = test_env.env_root().join("git-repo"); + let workspace_root = test_env.env_root().join("repo"); + let git_repo = git2::Repository::init(&git_repo_path).unwrap(); + // Must create a commit so we can create a worktree + let initial_commit = create_commit(&git_repo, "initial commit", &[]); + let _worktree = git_repo + .worktree("jj-worktree", &workspace_root, None) + .unwrap(); + assert!(workspace_root.join(".git").is_file()); + + // now commit in the worktree, so we know where we are importing from + let worktree_repo = git2::Repository::open(&workspace_root).unwrap(); + let _initial_commit = create_commit( + &worktree_repo, + "second commit", + &[&worktree_repo.find_commit(initial_commit).unwrap()], + ) + .to_string(); + + let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "init", "--git-repo", "."]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Done importing changes from the underlying Git repo. + Initialized repo in "." + "#); + + assert_eq!(read_git_target(&workspace_root), "../../../.git"); + + // The local ".git" repository is related, so commits should be imported + let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root); + insta::assert_snapshot!(stdout, @r#" + @ 58a55009f1c1 + ○ 49c630103a76 jj-worktree git_head() second commit + ○ 70618e4d103f master initial commit + ◆ 000000000000 + "#); + insta::assert_snapshot!(stderr, @""); + + // Check that Git HEAD is advanced because this is colocated + test_env.jj_cmd_ok(&workspace_root, &["new"]); + let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root); + insta::assert_snapshot!(stdout, @r#" + @ f133c02dde73 + ○ 58a55009f1c1 git_head() + ○ 49c630103a76 jj-worktree second commit + ○ 70618e4d103f master initial commit + ◆ 000000000000 + "#); + insta::assert_snapshot!(stderr, @""); + + let stdout = test_env.jj_cmd_success(&workspace_root, OP_LOG_COMPACT); + insta::assert_snapshot!(stdout, @r#" + @ 0c47efd49cba new empty commit + │ args: jj new + ○ 5fd008dad5b5 import git head + │ args: jj git init --git-repo . + ○ ec635623258d import git refs + │ args: jj git init --git-repo . + ○ eac759b9ab75 add workspace 'default' + ○ 000000000000 + "#); +} + +const OP_LOG_COMPACT: &[&str] = &[ + "op", + "log", + "-Tself.id().short() ++ ' ' ++ separate(\"\\n\", self.description().first_line(), self.tags())", +]; + +/// This one is a bit weird, but technically you can do it. Should be roughly +/// equivalent to the --git-repo=. case, but with a different git_target file. +#[test] +fn test_git_init_external_in_worktree_pointing_commondir() { + let test_env = TestEnvironment::default(); + let git_repo_path = test_env.env_root().join("git-repo"); + let workspace_root = test_env.env_root().join("repo"); + let git_repo = git2::Repository::init(&git_repo_path).unwrap(); + // Must create a commit so we can create a worktree + let initial_commit = create_commit(&git_repo, "initial commit", &[]); + let _worktree = git_repo + .worktree("jj-worktree", &workspace_root, None) + .unwrap(); + assert!(workspace_root.join(".git").is_file()); + + // now commit in the worktree, so we know where we are importing from + let worktree_repo = git2::Repository::open(&workspace_root).unwrap(); + let _initial_commit = create_commit( + &worktree_repo, + "second commit", + &[&worktree_repo.find_commit(initial_commit).unwrap()], + ) + .to_string(); + + let (stdout, stderr) = test_env.jj_cmd_ok( + &workspace_root, + &["git", "init", "--git-repo", "../git-repo"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Done importing changes from the underlying Git repo. + Initialized repo in "." + "#); + + assert_eq!( + PathBuf::from(read_git_target(&workspace_root)) + .canonicalize() + .unwrap(), + git_repo_path.join(".git") + ); + + // The local ".git" repository is related, so commits should be imported, + // specifically from the worktree, not the original repo. + let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root); + insta::assert_snapshot!(stdout, @r#" + @ 58a55009f1c1 + ○ 49c630103a76 jj-worktree git_head() second commit + ○ 70618e4d103f master initial commit + ◆ 000000000000 + "#); + insta::assert_snapshot!(stderr, @""); + + // Check that Git HEAD is advanced because this is colocated + test_env.jj_cmd_ok(&workspace_root, &["new"]); + let (stdout, stderr) = get_log_output_with_stderr(&test_env, &workspace_root); + insta::assert_snapshot!(stdout, @r#" + @ f133c02dde73 + ○ 58a55009f1c1 git_head() + ○ 49c630103a76 jj-worktree second commit + ○ 70618e4d103f master initial commit + ◆ 000000000000 + "#); + insta::assert_snapshot!(stderr, @""); + + let stdout = test_env.jj_cmd_success(&workspace_root, OP_LOG_COMPACT); + insta::assert_snapshot!(stdout, @r#" + @ 79619167098b new empty commit + │ args: jj new + ○ db043dbad297 import git head + │ args: jj git init --git-repo ../git-repo + ○ 7296ad45aee9 import git refs + │ args: jj git init --git-repo ../git-repo + ○ eac759b9ab75 add workspace 'default' + ○ 000000000000 + "#); +} + #[test] fn test_git_init_colocated_via_flag_git_dir_exists() { let test_env = TestEnvironment::default(); diff --git a/lib/src/git.rs b/lib/src/git.rs index 0492891de..d10090c65 100644 --- a/lib/src/git.rs +++ b/lib/src/git.rs @@ -1863,46 +1863,45 @@ impl MaybeColocatedGitRepo { Self { colocated, ..self } } + /// Try to open `/.git` as a git repository. + /// + /// If it succeeds, and the commondir matches JJ's backing repo, then the + /// workspace is colocated, and we return the newly opened repository. + /// + /// Easy to sanity check with git -- if `git` works and addresses the same + /// underlying repo (commondir), then the workspace will be colocated. So + /// things like worktrees, symlinks, etc just work. fn try_detect_colocated_workspace( self, workspace_root: &Path, - _open_opts: gix::open::Options, + open_opts: gix::open::Options, ) -> Self { let store_repo = &self.git_repo; - // The git backend may be a bare repository if we created it without --colocate - // but then created a workspace using `jj workspace add --colocate ../second` - let git_backend_workdir = store_repo.work_dir(); - - // 1. Check if we are in a colocated workspace, specifically the one that has - // both .git and .jj/repo. - // -------------------------------------------------------------------------- - - // Fast path -- the paths are the same, without looking through symlinks. - if git_backend_workdir == Some(workspace_root) { - // We are in a colocated workspace that's home to the real .git directory. - // e.g. /repo with /repo/.git - return self.with_colocated(true); - } - - // Otherwise, canonicalize both the git backend workdir and the workspace - let git_backend_workdir_canonical = git_backend_workdir.and_then(|p| p.canonicalize().ok()); - - // Colocated workspace should have ".git" directory, file, or symlink. Compare - // its parent as the git_workdir might be resolved from the real ".git" path. - let workspace_dot_git = workspace_root.join(".git"); - let Ok(workspace_dot_git_canonical) = workspace_dot_git.canonicalize() else { + let Ok(workspace_repo) = + gix::ThreadSafeRepository::open_opts(workspace_root.join(".git"), open_opts) + else { + // If gix can't open it, we are not colocated. return self.with_colocated(false); }; - // i.e. (/symlink_to_repo -> /repo).canonicalize() == (/repo/.git).parent() - if let Some(gbw) = git_backend_workdir_canonical.as_deref() { - if workspace_dot_git_canonical.parent() == Some(gbw) { - // This is the default workspace of a colocated repo - return self.with_colocated(true); - } + // Especially for worktrees, common_dir() returns paths with ../.. in them, + // usually. Must canonicalize. + let Ok(workspace_common_dir) = workspace_repo.to_thread_local().common_dir().canonicalize() + else { + return self.with_colocated(false); + }; + let Ok(store_common_dir) = store_repo.to_thread_local().common_dir().canonicalize() else { + return self.with_colocated(false); + }; + + if workspace_common_dir != store_common_dir { + return self.with_colocated(false); } - self.with_colocated(false) + Self { + git_repo: workspace_repo, + colocated: true, + } } }