Skip to content

Commit

Permalink
cli: unblock "jj git init --colocate" in existing Git repo directory
Browse files Browse the repository at this point in the history
I'm not sure what's the conclusion in jj-vcs#2747, but I don't think there is a
disagreement on allowing --colocate to import existing Git repo.
  • Loading branch information
yuja committed Feb 27, 2024
1 parent e3c58de commit e18c96f
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 60 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* When creating a new workspace, the sparse patterns are now copied over from
the current workspace.

* `jj git init --colocate` can now import an existing Git repository. This is
equivalent to `jj git init --git-repo=.`.

* `jj git fetch` now automatically prints new remote branches and tags by default.

* `--verbose/-v` is now `--debug` (no short option since it's not intended to be used often)
Expand Down
2 changes: 1 addition & 1 deletion cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1818,7 +1818,7 @@ fn map_workspace_load_error(err: WorkspaceLoadError, workspace_path: Option<&str
message,
"It looks like this is a git repo. You can create a jj repo backed by it by \
running this:
jj git init --git-repo=.",
jj git init --colocate",
)
} else {
user_error(message)
Expand Down
96 changes: 54 additions & 42 deletions cli/src/commands/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,9 @@ pub struct GitInitArgs {
/// `git` repo, allowing the use of both `jj` and `git` commands
/// in the same directory.
///
/// This is done by placing the backing git repo into a `.git` directory
/// in the root of the `jj` repo along with the `.jj` directory.
///
/// This option is only valid when creating new repos. To
/// reuse an existing `.git` directory in an existing git
/// repo, see the `--git-repo` param below.
/// This is done by placing the backing git repo into a `.git` directory in
/// the root of the `jj` repo along with the `.jj` directory. If the `.git`
/// directory already exists, all the existing commits will be imported.
///
/// This option is mutually exclusive with `--git-repo`.
#[arg(long, conflicts_with = "git_repo")]
Expand Down Expand Up @@ -379,52 +376,67 @@ pub fn git_init(
colocate: bool,
git_repo: Option<&str>,
) -> Result<(), CommandError> {
if colocate {
let (workspace, repo) = Workspace::init_colocated_git(command.settings(), workspace_root)?;
let workspace_command = command.for_loaded_repo(ui, workspace, repo)?;
maybe_add_gitignore(&workspace_command)?;
return Ok(());
#[derive(Clone, Debug)]
enum GitInitMode {
Colocate,
External(PathBuf),
Internal,
}

if let Some(git_store_str) = git_repo {
let git_store_path = command.cwd().join(git_store_str);
let (workspace, repo) =
Workspace::init_external_git(command.settings(), workspace_root, &git_store_path)?;
// Import refs first so all the reachable commits are indexed in
// chronological order.
let colocated = is_colocated_git_workspace(&workspace, &repo);
let repo = init_git_refs(ui, command, repo, colocated)?;
let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?;
maybe_add_gitignore(&workspace_command)?;
workspace_command.maybe_snapshot(ui)?;
if !workspace_command.working_copy_shared_with_git() {
let mut tx = workspace_command.start_transaction();
jj_lib::git::import_head(tx.mut_repo())?;
if let Some(git_head_id) = tx.mut_repo().view().git_head().as_normal().cloned() {
let git_head_commit = tx.mut_repo().store().get_commit(&git_head_id)?;
tx.check_out(&git_head_commit)?;
}
if tx.mut_repo().has_changes() {
tx.finish(ui, "import git head")?;
}
let colocated_git_repo_path = workspace_root.join(".git");
let init_mode = if colocate {
if colocated_git_repo_path.exists() {
GitInitMode::External(colocated_git_repo_path)
} else {
GitInitMode::Colocate
}
print_trackable_remote_branches(ui, workspace_command.repo().view())?;
} else if let Some(path_str) = git_repo {
GitInitMode::External(command.cwd().join(path_str))
} else {
if workspace_root.join(".git").exists() {
let cwd = command.cwd().canonicalize().unwrap();
let relative_wc_path = file_util::relative_path(&cwd, workspace_root);
if colocated_git_repo_path.exists() {
return Err(user_error_with_hint(
"Did not create a jj repo because there is an existing Git repo in this directory.",
format!(
r#"To create a repo backed by the existing Git repo, run `jj git init --git-repo={}` instead."#,
relative_wc_path.display()
),
"To create a repo backed by the existing Git repo, run `jj git init --colocate` \
instead.",
));
}
GitInitMode::Internal
};

Workspace::init_internal_git(command.settings(), workspace_root)?;
match &init_mode {
GitInitMode::Colocate => {
let (workspace, repo) =
Workspace::init_colocated_git(command.settings(), workspace_root)?;
let workspace_command = command.for_loaded_repo(ui, workspace, repo)?;
maybe_add_gitignore(&workspace_command)?;
}
GitInitMode::External(git_repo_path) => {
let (workspace, repo) =
Workspace::init_external_git(command.settings(), workspace_root, git_repo_path)?;
// Import refs first so all the reachable commits are indexed in
// chronological order.
let colocated = is_colocated_git_workspace(&workspace, &repo);
let repo = init_git_refs(ui, command, repo, colocated)?;
let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?;
maybe_add_gitignore(&workspace_command)?;
workspace_command.maybe_snapshot(ui)?;
if !workspace_command.working_copy_shared_with_git() {
let mut tx = workspace_command.start_transaction();
jj_lib::git::import_head(tx.mut_repo())?;
if let Some(git_head_id) = tx.mut_repo().view().git_head().as_normal().cloned() {
let git_head_commit = tx.mut_repo().store().get_commit(&git_head_id)?;
tx.check_out(&git_head_commit)?;
}
if tx.mut_repo().has_changes() {
tx.finish(ui, "import git head")?;
}
}
print_trackable_remote_branches(ui, workspace_command.repo().view())?;
}
GitInitMode::Internal => {
Workspace::init_internal_git(command.settings(), workspace_root)?;
}
}

Ok(())
}

Expand Down
27 changes: 22 additions & 5 deletions cli/tests/test_git_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,12 +542,29 @@ fn test_git_init_colocated_via_flag_git_dir_exists() {
let workspace_root = test_env.env_root().join("repo");
init_git_repo(&workspace_root, false);

let stderr = test_env.jj_cmd_failure(&workspace_root, &["git", "init", "--colocate"]);
let (stdout, stderr) =
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Error: Failed to access the repository
Caused by:
1: Failed to initialize git repository
2: Refusing to initialize the existing '$TEST_ENV/repo/.git' directory
Done importing changes from the underlying Git repo.
Initialized repo in "repo"
"###);

// 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_ok(&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)
~
"###);
}

Expand Down
2 changes: 1 addition & 1 deletion cli/tests/test_global_opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ fn test_no_workspace_directory() {
insta::assert_snapshot!(stderr, @r###"
Error: There is no jj repo in "."
Hint: It looks like this is a git repo. You can create a jj repo backed by it by running this:
jj git init --git-repo=.
jj git init --colocate
"###);
}

Expand Down
2 changes: 1 addition & 1 deletion cli/tests/test_init_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ fn test_init_git_internal_must_be_colocated() {
let stderr = test_env.jj_cmd_failure(&workspace_root, &["init", "--git"]);
insta::assert_snapshot!(stderr, @r###"
Error: Did not create a jj repo because there is an existing Git repo in this directory.
Hint: To create a repo backed by the existing Git repo, run `jj git init --git-repo=.` instead.
Hint: To create a repo backed by the existing Git repo, run `jj git init --colocate` instead.
"###);
}

Expand Down
3 changes: 1 addition & 2 deletions docs/git-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,7 @@ parent.
<tbody>
<tr>
<td>Create a new repo</td>
<td><code>jj init --git</code> (without <code>--git</code>, you get a
native Jujutsu repo, which is slow and whose format will change)</td>
<td><code>jj git init [--colocate]</code></td>
<td><code>git init</code></td>
</tr>
<tr>
Expand Down
4 changes: 2 additions & 2 deletions docs/git-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ into a directory by the same name.
## Co-located Jujutsu/Git repos

A "co-located" Jujutsu repo is a hybrid Jujutsu/Git repo. These can be created
if you initialize the Jujutsu repo in an existing Git repo by running `jj init
--git-repo=.` or with `jj git clone --colocate`. The Git repo and the Jujutsu
if you initialize the Jujutsu repo in an existing Git repo by running `jj git
init --colocate` or with `jj git clone --colocate`. The Git repo and the Jujutsu
repo then share the same working copy. Jujutsu will import and export from and
to the Git repo on every `jj` command automatically.

Expand Down
11 changes: 5 additions & 6 deletions docs/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,11 @@ changes.

## Working in a Git co-located repository

After doing `jj init --git-repo=.`, Git will be in
a [detached HEAD state][detached], which is unusual, as Git mainly works with
branches. In a co-located repository, every `jj` command will automatically
synchronize Jujutsu's view of the repo with Git's view. For example,
`jj commit` updates the HEAD of the Git repository, enabling an incremental
migration.
After doing `jj git init --colocate`, Git will be in a [detached HEAD
state][detached], which is unusual, as Git mainly works with branches. In a
co-located repository, every `jj` command will automatically synchronize
Jujutsu's view of the repo with Git's view. For example, `jj commit` updates the
HEAD of the Git repository, enabling an incremental migration.

```shell
$ nvim docs/tutorial.md
Expand Down

0 comments on commit e18c96f

Please sign in to comment.