diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b4af51c..a1a6b98ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 920e2378d..3dc07af85 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -1829,7 +1829,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) diff --git a/cli/src/commands/git.rs b/cli/src/commands/git.rs index eeefbbb8b..b7f3cce6a 100644 --- a/cli/src/commands/git.rs +++ b/cli/src/commands/git.rs @@ -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")] @@ -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(()) } diff --git a/cli/tests/test_git_init.rs b/cli/tests/test_git_init.rs index 6b587dcaa..a14051b54 100644 --- a/cli/tests/test_git_init.rs +++ b/cli/tests/test_git_init.rs @@ -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 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_ok(&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) + ~ "###); } diff --git a/cli/tests/test_global_opts.rs b/cli/tests/test_global_opts.rs index f6bafbc4d..38345026e 100644 --- a/cli/tests/test_global_opts.rs +++ b/cli/tests/test_global_opts.rs @@ -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 "###); } diff --git a/cli/tests/test_init_command.rs b/cli/tests/test_init_command.rs index fd8921eb0..56ccf686c 100644 --- a/cli/tests/test_init_command.rs +++ b/cli/tests/test_init_command.rs @@ -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. "###); } diff --git a/docs/git-comparison.md b/docs/git-comparison.md index 9c8635b86..2d2d67a53 100644 --- a/docs/git-comparison.md +++ b/docs/git-comparison.md @@ -100,8 +100,7 @@ parent. Create a new repo - jj init --git (without --git, you get a - native Jujutsu repo, which is slow and whose format will change) + jj git init [--colocate] git init diff --git a/docs/git-compatibility.md b/docs/git-compatibility.md index 51375bb6c..a249148f4 100644 --- a/docs/git-compatibility.md +++ b/docs/git-compatibility.md @@ -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. diff --git a/docs/github.md b/docs/github.md index a50af917d..c5acc8dce 100644 --- a/docs/github.md +++ b/docs/github.md @@ -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