Skip to content

Commit

Permalink
commands: Support the option of colocating a git repo with a jj repo
Browse files Browse the repository at this point in the history
This adds the new --colocate flag to `jj git clone`.

```
jj git clone --colocate https://github.com/foo/bar
```

is effectively equivalent to:

```
git clone https://github.com/foo/bar
cd bar
jj init --git-repo=.
```
  • Loading branch information
poucet committed Jul 29, 2023
1 parent 9c8d8b7 commit 56e6233
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 4 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions src/commands/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ pub struct GitCloneArgs {
/// The directory to write the Jujutsu repo to
#[arg(value_hint = clap::ValueHint::DirPath)]
destination: Option<String>,
/// Whether or not to colocate the Jujutsu repo with the git repo
#[arg(long)]
colocate: bool,
}

/// Push to a Git remote
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<String>), 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)?;
Expand Down
108 changes: 107 additions & 1 deletion tests/test_git_clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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", "[email protected]", &git2::Time::new(0, 0)).unwrap();
let mut tree_builder = git_repo.treebuilder(None).unwrap();
Expand Down Expand Up @@ -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", "[email protected]", &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
"###);
}

0 comments on commit 56e6233

Please sign in to comment.