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]