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
This commit is contained in:
Yuya Nishihara 2023-08-10 09:11:17 +09:00
parent 900300cf5f
commit b2101d15c8
3 changed files with 175 additions and 13 deletions

View file

@ -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

View file

@ -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::<GitBackend>();
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::<GitBackend>() 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<ReadonlyRepo>,
settings: &UserSettings,

View file

@ -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();
// <workspace_root>/.git -> <git_repo_path>
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();
// <workspace_root>/.git -> <git_repo_path>
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();
// <workspace_root>/.git -> <git_workdir_path>/.git -> <git_repo_path>
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]