diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ee82219..19408d8ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj log` output is now topologically grouped. [#242](https://github.com/martinvonz/jj/issues/242) +* `jj git clone` now supports the `--colocate` flag to create the git repo + in the same directory as the jj repo. + ### Fixed bugs ## [0.8.0] - 2023-07-09 diff --git a/src/commands/git.rs b/src/commands/git.rs index 600c712f8..8df1dd6d6 100644 --- a/src/commands/git.rs +++ b/src/commands/git.rs @@ -115,6 +115,9 @@ pub struct GitCloneArgs { /// The directory to write the Jujutsu repo to #[arg(value_hint = clap::ValueHint::DirPath)] destination: Option, + /// Whether or not to colocate the Jujutsu repo with the git repo + #[arg(long)] + colocate: bool, } /// Push to a Git remote @@ -410,11 +413,11 @@ fn cmd_git_clone( fs::create_dir(&wc_path).unwrap(); } - let clone_result = do_git_clone(ui, command, &source, &wc_path); + let canonical_wc_path: PathBuf = wc_path.canonicalize().unwrap(); + let clone_result = do_git_clone(ui, command, args.colocate, &source, &canonical_wc_path); if clone_result.is_err() { // Canonicalize because fs::remove_dir_all() doesn't seem to like e.g. // `/some/path/.` - let canonical_wc_path = wc_path.canonicalize().unwrap(); if let Err(err) = fs::remove_dir_all(canonical_wc_path.join(".jj")).and_then(|_| { if !wc_path_existed { fs::remove_dir(&canonical_wc_path) @@ -452,10 +455,16 @@ fn cmd_git_clone( fn do_git_clone( ui: &mut Ui, command: &CommandHelper, + colocate: bool, source: &str, wc_path: &Path, ) -> Result<(WorkspaceCommandHelper, Option), CommandError> { - let (workspace, repo) = Workspace::init_internal_git(command.settings(), wc_path)?; + let (workspace, repo) = if colocate { + let git_repo = git2::Repository::init(wc_path)?; + Workspace::init_external_git(command.settings(), wc_path, git_repo.path())? + } else { + Workspace::init_internal_git(command.settings(), wc_path)? + }; let git_repo = get_git_repo(repo.store())?; writeln!(ui, r#"Fetching into new repo in "{}""#, wc_path.display())?; let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?; diff --git a/tests/test_git_clone.rs b/tests/test_git_clone.rs index 41b59249b..c3118a16f 100644 --- a/tests/test_git_clone.rs +++ b/tests/test_git_clone.rs @@ -29,7 +29,7 @@ fn test_git_clone() { Nothing changed. "###); - // Clone a non-empty repo + // Set-up a non-empty repo let signature = git2::Signature::new("Some One", "some.one@example.com", &git2::Time::new(0, 0)).unwrap(); let mut tree_builder = git_repo.treebuilder(None).unwrap(); @@ -87,3 +87,109 @@ fn test_git_clone() { Error: Destination path exists and is not an empty directory "###); } + +#[test] +fn test_git_clone_colocate() { + let test_env = TestEnvironment::default(); + let git_repo_path = test_env.env_root().join("source"); + let git_repo = git2::Repository::init(git_repo_path).unwrap(); + + // Clone an empty repo + let stdout = test_env.jj_cmd_success( + test_env.env_root(), + &["git", "clone", "source", "empty", "--colocate"], + ); + insta::assert_snapshot!(stdout, @r###" + Fetching into new repo in "$TEST_ENV/empty" + Nothing changed. + "###); + + // Set-up a non-empty repo + let signature = + git2::Signature::new("Some One", "some.one@example.com", &git2::Time::new(0, 0)).unwrap(); + let mut tree_builder = git_repo.treebuilder(None).unwrap(); + let file_oid = git_repo.blob(b"content").unwrap(); + tree_builder + .insert("file", file_oid, git2::FileMode::Blob.into()) + .unwrap(); + let tree_oid = tree_builder.write().unwrap(); + let tree = git_repo.find_tree(tree_oid).unwrap(); + git_repo + .commit( + Some("refs/heads/main"), + &signature, + &signature, + "message", + &tree, + &[], + ) + .unwrap(); + git_repo.set_head("refs/heads/main").unwrap(); + + // Clone with relative source path + let stdout = test_env.jj_cmd_success( + test_env.env_root(), + &["git", "clone", "source", "clone", "--colocate"], + ); + insta::assert_snapshot!(stdout, @r###" + Fetching into new repo in "$TEST_ENV/clone" + Working copy now at: 1f0b881a057d (no description set) + Parent commit : 9f01a0e04879 message + Added 1 files, modified 0 files, removed 0 files + "###); + assert!(test_env.env_root().join("clone").join("file").exists()); + assert!(test_env.env_root().join("clone").join(".git").exists()); + + eprintln!( + "{:?}", + git_repo.head().expect("Repo head should be set").name() + ); + + let jj_git_repo = git2::Repository::open(test_env.env_root().join("clone")) + .expect("Could not open clone repo"); + assert_eq!( + jj_git_repo + .head() + .expect("Clone Repo HEAD should be set.") + .symbolic_target(), + git_repo + .head() + .expect("Repo HEAD should be set.") + .symbolic_target() + ); + + // Subsequent fetch should just work even if the source path was relative + let stdout = test_env.jj_cmd_success(&test_env.env_root().join("clone"), &["git", "fetch"]); + insta::assert_snapshot!(stdout, @r###" + Nothing changed. + "###); + + // Try cloning into an existing workspace + let stderr = test_env.jj_cmd_failure( + test_env.env_root(), + &["git", "clone", "source", "clone", "--colocate"], + ); + insta::assert_snapshot!(stderr, @r###" + Error: Destination path exists and is not an empty directory + "###); + + // Try cloning into an existing file + std::fs::write(test_env.env_root().join("file"), "contents").unwrap(); + let stderr = test_env.jj_cmd_failure( + test_env.env_root(), + &["git", "clone", "source", "file", "--colocate"], + ); + insta::assert_snapshot!(stderr, @r###" + Error: Destination path exists and is not an empty directory + "###); + + // Try cloning into non-empty, non-workspace directory + std::fs::remove_dir_all(test_env.env_root().join("clone").join(".jj")).unwrap(); + let stderr = test_env.jj_cmd_failure( + test_env.env_root(), + &["git", "clone", "source", "clone", "--colocate"], + ); + insta::assert_snapshot!(stderr, @r###" + Error: Destination path exists and is not an empty directory + "###); +}