Skip to content

Commit

Permalink
cli: detect .git symlink as a colocated workspace
Browse files Browse the repository at this point in the history
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
  • Loading branch information
yuja committed Aug 10, 2023
1 parent 900300c commit b2101d1
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 18 additions & 8 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
158 changes: 153 additions & 5 deletions cli/tests/test_init_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@
// 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;

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
Expand Down Expand Up @@ -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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 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 [email protected] 2001-02-03 04:05:07.000 +07:00 230dd059
│ (empty) (no description set)
~
"###);
}

#[test]
Expand Down

0 comments on commit b2101d1

Please sign in to comment.