From b2101d15c81a64ede078333c955b48ae1aa92d3d Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Thu, 10 Aug 2023 09:11:17 +0900 Subject: [PATCH] cli: detect .git symlink as a colocated workspace Maybe we could load GitBackend without resolving .git symlink, but that would introduce more subtle bugs. Instead, we calculate the expected Git workdir path from the canonical ".git" path. Fixes #2011 --- CHANGELOG.md | 4 + cli/src/cli_util.rs | 26 ++++-- cli/tests/test_init_command.rs | 158 +++++++++++++++++++++++++++++++-- 3 files changed, 175 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e298a55..2f4cd1a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * SSH authentication can now use ed25519 and ed25519-sk keys. They still need to be password-less. +* Git repository managed by the repo tool can now be detected as a "colocated" + repository. + [#2011](https://github.com/martinvonz/jj/issues/2011) + ## [0.8.0] - 2023-07-09 ### Breaking changes diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index acc10bf8c..36a947609 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -696,14 +696,7 @@ impl WorkspaceCommandHelper { )?; let loaded_at_head = command.global_args.at_operation == "@"; let may_update_working_copy = loaded_at_head && !command.global_args.ignore_working_copy; - let mut working_copy_shared_with_git = false; - let maybe_git_backend = repo.store().backend_impl().downcast_ref::(); - if let Some(git_workdir) = maybe_git_backend - .and_then(|git_backend| git_backend.git_repo().workdir().map(ToOwned::to_owned)) - .and_then(|workdir| workdir.canonicalize().ok()) - { - working_copy_shared_with_git = git_workdir == workspace.workspace_root().as_path(); - } + let working_copy_shared_with_git = is_colocated_git_workspace(&workspace, &repo); Ok(Self { cwd: command.cwd.clone(), string_args: command.string_args.clone(), @@ -1599,6 +1592,23 @@ jj init --git-repo=.", } } +fn is_colocated_git_workspace(workspace: &Workspace, repo: &ReadonlyRepo) -> bool { + let Some(git_backend) = repo.store().backend_impl().downcast_ref::() else { + return false; + }; + let git_repo = git_backend.git_repo(); + let Some(git_workdir) = git_repo.workdir().and_then(|path| path.canonicalize().ok()) else { + return false; // Bare repository + }; + // Colocated workspace should have ".git" directory, file, or symlink. Since the + // backend is loaded from the canonical path, its working directory should also + // be resolved from the canonical ".git" path. + let Ok(dot_git_path) = workspace.workspace_root().join(".git").canonicalize() else { + return false; + }; + Some(git_workdir.as_ref()) == dot_git_path.parent() +} + pub fn start_repo_transaction( repo: &Arc, settings: &UserSettings, diff --git a/cli/tests/test_init_command.rs b/cli/tests/test_init_command.rs index fe3afdbce..7162742dd 100644 --- a/cli/tests/test_init_command.rs +++ b/cli/tests/test_init_command.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::PathBuf; +use std::path::Path; use test_case::test_case; @@ -20,10 +20,12 @@ use crate::common::TestEnvironment; pub mod common; -fn init_git_repo(git_repo_path: &PathBuf, bare: bool) { - let git_repo = - git2::Repository::init_opts(git_repo_path, git2::RepositoryInitOptions::new().bare(bare)) - .unwrap(); +fn init_git_repo(git_repo_path: &Path, bare: bool) { + init_git_repo_with_opts(git_repo_path, git2::RepositoryInitOptions::new().bare(bare)); +} + +fn init_git_repo_with_opts(git_repo_path: &Path, opts: &git2::RepositoryInitOptions) { + let git_repo = git2::Repository::init_opts(git_repo_path, opts).unwrap(); let git_blob_oid = git_repo.blob(b"some content").unwrap(); let mut git_tree_builder = git_repo.treebuilder(None).unwrap(); git_tree_builder @@ -185,6 +187,152 @@ fn test_init_git_colocated() { │ My commit message ~ "###); + + // Check that the Git repo's HEAD moves + test_env.jj_cmd_success(&workspace_root, &["new"]); + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ sqpuoqvx test.user@example.com 2001-02-03 04:05:07.000 +07:00 HEAD@git f61b77cd + │ (no description set) + ~ + "###); +} + +#[test] +fn test_init_git_colocated_gitlink() { + let test_env = TestEnvironment::default(); + // /.git -> + let git_repo_path = test_env.env_root().join("git-repo"); + let workspace_root = test_env.env_root().join("repo"); + init_git_repo_with_opts( + &git_repo_path, + git2::RepositoryInitOptions::new().workdir_path(&workspace_root), + ); + assert!(workspace_root.join(".git").is_file()); + let stdout = test_env.jj_cmd_success(&workspace_root, &["init", "--git-repo", "."]); + insta::assert_snapshot!(stdout, @r###" + Initialized repo in "." + "###); + + // Check that the Git repo's HEAD got checked out + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ mwrttmos git.user@example.com 1970-01-01 01:02:03.000 +01:00 my-branch HEAD@git 8d698d4a + │ My commit message + ~ + "###); + + // Check that the Git repo's HEAD moves + test_env.jj_cmd_success(&workspace_root, &["new"]); + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ sqpuoqvx test.user@example.com 2001-02-03 04:05:07.000 +07:00 HEAD@git f61b77cd + │ (no description set) + ~ + "###); +} + +#[cfg(unix)] +#[test] +fn test_init_git_colocated_symlink_directory() { + let test_env = TestEnvironment::default(); + // /.git -> + let git_repo_path = test_env.env_root().join("git-repo"); + let workspace_root = test_env.env_root().join("repo"); + init_git_repo(&git_repo_path, false); + std::fs::create_dir(&workspace_root).unwrap(); + std::os::unix::fs::symlink(git_repo_path.join(".git"), workspace_root.join(".git")).unwrap(); + let stdout = test_env.jj_cmd_success(&workspace_root, &["init", "--git-repo", "."]); + insta::assert_snapshot!(stdout, @r###" + Initialized repo in "." + "###); + + // Check that the Git repo's HEAD got checked out + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ mwrttmos git.user@example.com 1970-01-01 01:02:03.000 +01:00 my-branch HEAD@git 8d698d4a + │ My commit message + ~ + "###); + + // Check that the Git repo's HEAD moves + test_env.jj_cmd_success(&workspace_root, &["new"]); + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ sqpuoqvx test.user@example.com 2001-02-03 04:05:07.000 +07:00 HEAD@git f61b77cd + │ (no description set) + ~ + "###); +} + +#[cfg(unix)] +#[test] +fn test_init_git_colocated_symlink_gitlink() { + let test_env = TestEnvironment::default(); + // /.git -> /.git -> + let git_repo_path = test_env.env_root().join("git-repo"); + let git_workdir_path = test_env.env_root().join("git-workdir"); + let workspace_root = test_env.env_root().join("repo"); + init_git_repo_with_opts( + &git_repo_path, + git2::RepositoryInitOptions::new().workdir_path(&git_workdir_path), + ); + assert!(git_workdir_path.join(".git").is_file()); + std::fs::create_dir(&workspace_root).unwrap(); + std::os::unix::fs::symlink(git_workdir_path.join(".git"), workspace_root.join(".git")).unwrap(); + let stdout = test_env.jj_cmd_success(&workspace_root, &["init", "--git-repo", "."]); + insta::assert_snapshot!(stdout, @r###" + Initialized repo in "." + "###); + + // Check that the Git repo's HEAD got checked out + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ mwrttmos git.user@example.com 1970-01-01 01:02:03.000 +01:00 my-branch HEAD@git 8d698d4a + │ My commit message + ~ + "###); + + // Check that the Git repo's HEAD moves + test_env.jj_cmd_success(&workspace_root, &["new"]); + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ sqpuoqvx test.user@example.com 2001-02-03 04:05:07.000 +07:00 HEAD@git f61b77cd + │ (no description set) + ~ + "###); +} + +#[test] +fn test_init_git_external_but_git_dir_exists() { + 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"); + git2::Repository::init(&git_repo_path).unwrap(); + init_git_repo(&workspace_root, false); + let stdout = test_env.jj_cmd_success( + &workspace_root, + &["init", "--git-repo", git_repo_path.to_str().unwrap()], + ); + insta::assert_snapshot!(stdout, @r###" + Initialized repo in "." + "###); + + // The local ".git" repository is unrelated, so no commits should be imported + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ zzzzzzzz 1970-01-01 00:00:00.000 +00:00 00000000 + (empty) (no description set) + "###); + + // Check that Git HEAD is not set because this isn't a colocated repo + test_env.jj_cmd_success(&workspace_root, &["new"]); + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ qpvuntsm test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059 + │ (empty) (no description set) + ~ + "###); } #[test]