From a7fce2ab6f7f5058ee67b4161046cafd1cd9dd6e Mon Sep 17 00:00:00 2001 From: Essien Ita Essien Date: Mon, 8 Jan 2024 10:41:07 +0000 Subject: [PATCH] cli: add a `jj git init` command. This initializes a git backed repo. * It does the same thing as `jj init --git` except that it has a --colocated flag to explicitly specify that we want the .git repo to be side-by-side the .jj repo in the working directory. * `jj init --git` will keep the current behaviour and will not be able to create colocated git backed repos. * Update test snapshots. --- CHANGELOG.md | 5 + cli/src/commands/git.rs | 84 ++++- cli/src/commands/init.rs | 18 +- cli/tests/cli-reference@.md.snap | 28 +- cli/tests/test_git_init.rs | 508 +++++++++++++++++++++++++++++++ cli/tests/test_init_command.rs | 24 +- 6 files changed, 658 insertions(+), 9 deletions(-) create mode 100644 cli/tests/test_git_init.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd6048313..e92884c269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj workspace root` was aliased to `jj root`, for ease of discoverability +* `jj git` now has an `init` command that initializes a git backed repo. + `jj init --git` and `jj init --git-repo` are now deprecated and + will be removed in the near future. + + ### Fixed bugs * Fixed snapshots of symlinks in `gitignore`-d directory. diff --git a/cli/src/commands/git.rs b/cli/src/commands/git.rs index a062a483bc..123f184d00 100644 --- a/cli/src/commands/git.rs +++ b/cli/src/commands/git.rs @@ -58,6 +58,7 @@ use crate::ui::Ui; pub enum GitCommand { #[command(subcommand)] Remote(GitRemoteCommand), + Init(GitInitArgs), Fetch(GitFetchArgs), Clone(GitCloneArgs), Push(GitPushArgs), @@ -107,6 +108,49 @@ pub struct GitRemoteRenameArgs { #[derive(clap::Args, Clone, Debug)] pub struct GitRemoteListArgs {} +/// Create a new Git backed repo. +#[derive(clap::Args, Clone, Debug)] +pub struct GitInitArgs { + /// The destination directory where the `jj` repo will be created. + /// If the directory does not exist, it will be created. + /// If no directory is diven, the current directory is used. + /// + /// By default the `git` repo is under `$destination/.jj` + #[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)] + destination: String, + + /// Specifies that the `jj` repo should also be a valid + /// `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 option is mutually exclusive with `--git-repo`. + #[arg(long, conflicts_with = "git_repo")] + colocated: bool, + + /// Specifies a path to an **existing** git repository to be + /// used as the backing git repo for the newly created `jj` repo. + /// + /// If the specified `--git-repo` path happens to be the same as + /// the `jj` repo path (both .jj and .git directories are in the + /// same working directory), then both `jj` and `git` commands + /// will work on the repo, with the exception that changes from `jj` + /// will not be auto-exported to the git repo. + /// + /// Auto-exporting from `jj` to `git` is only enabled for new repos. + /// See `--colocated` above. + /// + /// This option is mutually exclusive with `--colocated`. + #[arg(long, conflicts_with = "colocated", value_hint = clap::ValueHint::DirPath)] + git_repo: Option, +} + /// Fetch from a Git remote #[derive(clap::Args, Clone, Debug)] pub struct GitFetchArgs { @@ -317,11 +361,19 @@ pub fn git_init( ui: &mut Ui, command: &CommandHelper, workspace_root: &Path, + colocated: bool, git_repo: Option<&str>, ) -> Result<(), CommandError> { let cwd = command.cwd().canonicalize().unwrap(); let relative_wc_path = file_util::relative_path(&cwd, workspace_root); + if colocated { + 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(()); + } + if let Some(git_store_str) = git_repo { let git_store_path = cwd.join(git_store_str); let (workspace, repo) = @@ -349,7 +401,7 @@ pub fn git_init( 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 init --git-repo={}` instead."#, + r#"To create a repo backed by the existing Git repo, run `jj git init --git-repo={}` instead."#, relative_wc_path.display() ), )); @@ -361,6 +413,35 @@ pub fn git_init( Ok(()) } +fn cmd_git_init( + ui: &mut Ui, + command: &CommandHelper, + args: &GitInitArgs, +) -> Result<(), CommandError> { + let cwd = command.cwd().canonicalize().unwrap(); + let wc_path = cwd.join(&args.destination); + let wc_path = file_util::create_or_reuse_dir(&wc_path) + .and_then(|_| wc_path.canonicalize()) + .map_err(|e| user_error_with_message("Failed to create workspace", e))?; + + git_init( + ui, + command, + &wc_path, + args.colocated, + args.git_repo.as_deref(), + )?; + + let relative_wc_path = file_util::relative_path(&cwd, &wc_path); + writeln!( + ui.stderr(), + r#"Initialized repo in "{}""#, + relative_wc_path.display() + )?; + + Ok(()) +} + #[tracing::instrument(skip(ui, command))] fn cmd_git_fetch( ui: &mut Ui, @@ -1106,6 +1187,7 @@ pub fn cmd_git( subcommand: &GitCommand, ) -> Result<(), CommandError> { match subcommand { + GitCommand::Init(args) => cmd_git_init(ui, command, args), GitCommand::Fetch(args) => cmd_git_fetch(ui, command, args), GitCommand::Clone(args) => cmd_git_clone(ui, command, args), GitCommand::Remote(GitRemoteCommand::Add(args)) => cmd_git_remote_add(ui, command, args), diff --git a/cli/src/commands/init.rs b/cli/src/commands/init.rs index 9fd7a36e44..dfac8eb7ee 100644 --- a/cli/src/commands/init.rs +++ b/cli/src/commands/init.rs @@ -33,11 +33,13 @@ pub(crate) struct InitArgs { /// The destination directory #[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)] destination: String, + /// DEPRECATED: Use `jj git init` /// Use the Git backend, creating a jj repo backed by a Git repo - #[arg(long)] + #[arg(long, hide = true)] git: bool, + /// DEPRECATED: Use `jj git init` /// Path to a git repo the jj repo will be backed by - #[arg(long, value_hint = clap::ValueHint::DirPath)] + #[arg(long, hide = true, value_hint = clap::ValueHint::DirPath)] git_repo: Option, } @@ -53,13 +55,21 @@ pub(crate) fn cmd_init( .and_then(|_| wc_path.canonicalize()) .map_err(|e| user_error_with_message("Failed to create workspace", e))?; + // Preserve existing behaviour where `jj init` is not able to create + // a colocated repo. + let colocated = false; if args.git || args.git_repo.is_some() { - git::git_init(ui, command, &wc_path, args.git_repo.as_deref())?; + git::git_init(ui, command, &wc_path, colocated, args.git_repo.as_deref())?; + writeln!( + ui.warning(), + "warning: `--git` and `--git-repo` are deprecated. +Use `jj git init` to create git repos" + )?; } else { if !command.settings().allow_native_backend() { return Err(user_error_with_hint( "The native backend is disallowed by default.", - "Did you mean to pass `--git`? + "Did you mean to call `jj git init`? Set `ui.allow-init-native` to allow initializing a repo with the native backend.", )); } diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index b042c4a1b4..2c77207d2e 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -49,6 +49,7 @@ This document contains the help content for the `jj` command-line program. * [`jj git remote remove`↴](#jj-git-remote-remove) * [`jj git remote rename`↴](#jj-git-remote-rename) * [`jj git remote list`↴](#jj-git-remote-list) +* [`jj git init`↴](#jj-git-init) * [`jj git fetch`↴](#jj-git-fetch) * [`jj git clone`↴](#jj-git-clone) * [`jj git push`↴](#jj-git-push) @@ -746,6 +747,7 @@ For a comparison with Git, including a table of commands, see https://github.com ###### **Subcommands:** * `remote` — Manage Git remotes +* `init` — Create a new Git backed repo * `fetch` — Fetch from a Git remote * `clone` — Create a new repo backed by a clone of a Git repo * `push` — Push to a Git remote @@ -817,6 +819,28 @@ List Git remotes +## `jj git init` + +Create a new Git backed repo + +**Usage:** `jj git init [OPTIONS] [DESTINATION]` + +###### **Arguments:** + +* `` — The destination directory where the `jj` repo will be created. If the directory does not exist, it will be created. If no directory is diven, the current directory is used + + Default value: `.` + +###### **Options:** + +* `--colocated` — Specifies that the `jj` repo should also be a valid `git` repo, allowing the use of both `jj` and `git` commands in the same directory + + Possible values: `true`, `false` + +* `--git-repo ` — Specifies a path to an **existing** git repository to be used as the backing git repo for the newly created `jj` repo + + + ## `jj git fetch` Fetch from a Git remote @@ -919,11 +943,11 @@ If the given directory does not exist, it will be created. If no directory is gi ###### **Options:** -* `--git` — Use the Git backend, creating a jj repo backed by a Git repo +* `--git` — DEPRECATED: Use `jj git init` Use the Git backend, creating a jj repo backed by a Git repo Possible values: `true`, `false` -* `--git-repo ` — Path to a git repo the jj repo will be backed by +* `--git-repo ` — DEPRECATED: Use `jj git init` Path to a git repo the jj repo will be backed by diff --git a/cli/tests/test_git_init.rs b/cli/tests/test_git_init.rs new file mode 100644 index 0000000000..1eeef9d34b --- /dev/null +++ b/cli/tests/test_git_init.rs @@ -0,0 +1,508 @@ +// Copyright 2020 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::{Path, PathBuf}; + +use test_case::test_case; + +use crate::common::TestEnvironment; + +pub mod common; + +fn init_git_repo(git_repo_path: &Path, bare: bool) -> git2::Repository { + 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, +) -> git2::Repository { + 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 + .insert("some-file", git_blob_oid, 0o100644) + .unwrap(); + let git_tree_id = git_tree_builder.write().unwrap(); + drop(git_tree_builder); + let git_tree = git_repo.find_tree(git_tree_id).unwrap(); + let git_signature = git2::Signature::new( + "Git User", + "git.user@example.com", + &git2::Time::new(123, 60), + ) + .unwrap(); + git_repo + .commit( + Some("refs/heads/my-branch"), + &git_signature, + &git_signature, + "My commit message", + &git_tree, + &[], + ) + .unwrap(); + drop(git_tree); + git_repo.set_head("refs/heads/my-branch").unwrap(); + git_repo +} + +fn get_branch_output(test_env: &TestEnvironment, repo_path: &Path) -> String { + test_env.jj_cmd_success(repo_path, &["branch", "list", "--all"]) +} + +fn read_git_target(workspace_root: &Path) -> String { + let mut path = workspace_root.to_path_buf(); + path.extend([".jj", "repo", "store", "git_target"]); + std::fs::read_to_string(path).unwrap() +} + +#[test] +fn test_git_init_internal() { + let test_env = TestEnvironment::default(); + let (stdout, stderr) = test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Initialized repo in "repo" + "###); + + let workspace_root = test_env.env_root().join("repo"); + let jj_path = workspace_root.join(".jj"); + let repo_path = jj_path.join("repo"); + let store_path = repo_path.join("store"); + assert!(workspace_root.is_dir()); + assert!(jj_path.is_dir()); + assert!(jj_path.join("working_copy").is_dir()); + assert!(repo_path.is_dir()); + assert!(store_path.is_dir()); + assert!(store_path.join("git").is_dir()); + assert_eq!(read_git_target(&workspace_root), "git"); +} + +#[test_case(false; "full")] +#[test_case(true; "bare")] +fn test_git_init_external(bare: bool) { + let test_env = TestEnvironment::default(); + let git_repo_path = test_env.env_root().join("git-repo"); + init_git_repo(&git_repo_path, bare); + + let (stdout, stderr) = test_env.jj_cmd_ok( + test_env.env_root(), + &[ + "git", + "init", + "repo", + "--git-repo", + git_repo_path.to_str().unwrap(), + ], + ); + insta::allow_duplicates! { + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Done importing changes from the underlying Git repo. + Working copy now at: sqpuoqvx f6950fc1 (empty) (no description set) + Parent commit : mwrttmos 8d698d4a my-branch | My commit message + Added 1 files, modified 0 files, removed 0 files + Initialized repo in "repo" + "###); + } + + let workspace_root = test_env.env_root().join("repo"); + let jj_path = workspace_root.join(".jj"); + let repo_path = jj_path.join("repo"); + let store_path = repo_path.join("store"); + assert!(workspace_root.is_dir()); + assert!(jj_path.is_dir()); + assert!(jj_path.join("working_copy").is_dir()); + assert!(repo_path.is_dir()); + assert!(store_path.is_dir()); + let unix_git_target_file_contents = read_git_target(&workspace_root).replace('\\', "/"); + if bare { + assert!(unix_git_target_file_contents.ends_with("/git-repo")); + } else { + assert!(unix_git_target_file_contents.ends_with("/git-repo/.git")); + } + + // Check that the Git repo's HEAD got checked out + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "@-"]); + insta::allow_duplicates! { + 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 + ~ + "###); + } +} + +#[test] +fn test_git_init_external_non_existent_directory() { + let test_env = TestEnvironment::default(); + let stderr = test_env.jj_cmd_failure( + test_env.env_root(), + &["git", "init", "repo", "--git-repo", "non-existent"], + ); + insta::assert_snapshot!(stderr, @r###" + Error: $TEST_ENV/non-existent doesn't exist + "###); +} + +#[test] +fn test_git_init_external_non_existent_git_directory() { + let test_env = TestEnvironment::default(); + let workspace_root = test_env.env_root().join("repo"); + let stderr = test_env.jj_cmd_failure( + test_env.env_root(), + &["git", "init", "repo", "--git-repo", "repo"], + ); + + insta::assert_snapshot!(&stderr, @r###" + Error: Failed to access the repository + Caused by: + 1: Failed to open git repository + 2: "$TEST_ENV/repo" does not appear to be a git repository + 3: Missing HEAD at '.git/HEAD' + "###); + let jj_path = workspace_root.join(".jj"); + assert!(!jj_path.exists()); +} + +#[test] +fn test_git_init_colocated_via_git_repo_path() { + let test_env = TestEnvironment::default(); + let workspace_root = test_env.env_root().join("repo"); + init_git_repo(&workspace_root, false); + let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "init", "--git-repo", "."]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Done importing changes from the underlying Git repo. + Initialized repo in "." + "###); + + let jj_path = workspace_root.join(".jj"); + let repo_path = jj_path.join("repo"); + let store_path = repo_path.join("store"); + assert!(workspace_root.is_dir()); + assert!(jj_path.is_dir()); + assert!(jj_path.join("working_copy").is_dir()); + assert!(repo_path.is_dir()); + assert!(store_path.is_dir()); + assert!(read_git_target(&workspace_root) + .replace('\\', "/") + .ends_with("../../../.git")); + + // Check that the Git repo's HEAD got checked out + let stdout = test_env.jj_cmd_success(&repo_path, &["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) + ~ + "###); +} + +#[test] +fn test_git_init_colocated_via_git_repo_path_gitlink() { + let test_env = TestEnvironment::default(); + // /.git -> + 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, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "init", "--git-repo", "."]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Done importing changes from the underlying Git repo. + Initialized repo in "." + "###); + insta::assert_snapshot!(read_git_target(&workspace_root), @"../../../.git"); + + // 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) + ~ + "###); +} + +#[cfg(unix)] +#[test] +fn test_git_init_colocated_via_git_repo_path_symlink_directory() { + let test_env = TestEnvironment::default(); + // /.git -> + 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, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "init", "--git-repo", "."]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Done importing changes from the underlying Git repo. + Initialized repo in "." + "###); + insta::assert_snapshot!(read_git_target(&workspace_root), @"../../../.git"); + + // 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) + ~ + "###); +} + +#[cfg(unix)] +#[test] +fn test_git_init_colocated_via_git_repo_path_symlink_directory_without_bare_config() { + let test_env = TestEnvironment::default(); + // /.git -> + let git_repo_path = test_env.env_root().join("git-repo.git"); + let workspace_root = test_env.env_root().join("repo"); + // Set up git repo without core.bare set (as the "repo" tool would do.) + // The core.bare config is deduced from the directory name. + let git_repo = init_git_repo(&workspace_root, false); + git_repo.config().unwrap().remove("core.bare").unwrap(); + std::fs::rename(workspace_root.join(".git"), &git_repo_path).unwrap(); + std::os::unix::fs::symlink(&git_repo_path, workspace_root.join(".git")).unwrap(); + let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "init", "--git-repo", "."]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Done importing changes from the underlying Git repo. + Initialized repo in "." + "###); + insta::assert_snapshot!(read_git_target(&workspace_root), @"../../../.git"); + + // 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) + ~ + "###); +} + +#[cfg(unix)] +#[test] +fn test_git_init_colocated_via_git_repo_path_symlink_gitlink() { + let test_env = TestEnvironment::default(); + // /.git -> /.git -> + 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, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "init", "--git-repo", "."]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Done importing changes from the underlying Git repo. + Initialized repo in "." + "###); + insta::assert_snapshot!(read_git_target(&workspace_root), @"../../../.git"); + + // 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) + ~ + "###); +} + +#[test] +fn test_git_init_colocated_via_git_repo_path_imported_refs() { + let test_env = TestEnvironment::default(); + test_env.add_config("git.auto-local-branch = true"); + + // Set up remote refs + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "remote"]); + let remote_path = test_env.env_root().join("remote"); + test_env.jj_cmd_ok( + &remote_path, + &["branch", "create", "local-remote", "remote-only"], + ); + test_env.jj_cmd_ok(&remote_path, &["new"]); + test_env.jj_cmd_ok(&remote_path, &["git", "export"]); + + let remote_git_path = remote_path.join(PathBuf::from_iter([".jj", "repo", "store", "git"])); + let set_up_local_repo = |local_path: &Path| { + let git_repo = + git2::Repository::clone(remote_git_path.to_str().unwrap(), local_path).unwrap(); + let git_ref = git_repo + .find_reference("refs/remotes/origin/local-remote") + .unwrap(); + git_repo + .reference( + "refs/heads/local-remote", + git_ref.target().unwrap(), + false, + "", + ) + .unwrap(); + }; + + // With git.auto-local-branch = true + let local_path = test_env.env_root().join("local1"); + set_up_local_repo(&local_path); + let (_stdout, stderr) = test_env.jj_cmd_ok(&local_path, &["git", "init", "--git-repo=."]); + insta::assert_snapshot!(stderr, @r###" + Done importing changes from the underlying Git repo. + Initialized repo in "." + "###); + insta::assert_snapshot!(get_branch_output(&test_env, &local_path), @r###" + local-remote: vvkvtnvv 230dd059 (empty) (no description set) + @git: vvkvtnvv 230dd059 (empty) (no description set) + @origin: vvkvtnvv 230dd059 (empty) (no description set) + remote-only: vvkvtnvv 230dd059 (empty) (no description set) + @git: vvkvtnvv 230dd059 (empty) (no description set) + @origin: vvkvtnvv 230dd059 (empty) (no description set) + "###); + + // With git.auto-local-branch = false + test_env.add_config("git.auto-local-branch = false"); + let local_path = test_env.env_root().join("local2"); + set_up_local_repo(&local_path); + let (_stdout, stderr) = test_env.jj_cmd_ok(&local_path, &["git", "init", "--git-repo=."]); + insta::assert_snapshot!(stderr, @r###" + Done importing changes from the underlying Git repo. + The following remote branches aren't associated with the existing local branches: + local-remote@origin + Hint: Run `jj branch track local-remote@origin` to keep local branches updated on future pulls. + Initialized repo in "." + "###); + insta::assert_snapshot!(get_branch_output(&test_env, &local_path), @r###" + local-remote: vvkvtnvv 230dd059 (empty) (no description set) + @git: vvkvtnvv 230dd059 (empty) (no description set) + local-remote@origin: vvkvtnvv 230dd059 (empty) (no description set) + remote-only@origin: vvkvtnvv 230dd059 (empty) (no description set) + "###); +} + +#[test] +fn test_git_init_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, stderr) = test_env.jj_cmd_ok( + &workspace_root, + &["git", "init", "--git-repo", git_repo_path.to_str().unwrap()], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @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 root() 00000000 + "###); + + // Check that Git HEAD is not set because this isn't a colocated repo + test_env.jj_cmd_ok(&workspace_root, &["new"]); + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + ◉ qpvuntsm test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059 + │ (empty) (no description set) + ~ + "###); +} + +#[test] +fn test_git_init_colocated_via_flag_git_dir_exists() { + let test_env = TestEnvironment::default(); + 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", "--colocated"]); + 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 + "###); +} + +#[test] +fn test_git_init_colocated_via_flag_git_dir_not_exists() { + let test_env = TestEnvironment::default(); + let (stdout, stderr) = + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocated", "repo"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Initialized repo in "repo" + "###); +} + +#[test] +fn test_git_init_bad_wc_path() { + let test_env = TestEnvironment::default(); + std::fs::write(test_env.env_root().join("existing-file"), b"").unwrap(); + let stderr = test_env.jj_cmd_failure(test_env.env_root(), &["git", "init", "existing-file"]); + assert!(stderr.contains("Failed to create workspace")); +} diff --git a/cli/tests/test_init_command.rs b/cli/tests/test_init_command.rs index 4e994705bd..34a2aaa2d7 100644 --- a/cli/tests/test_init_command.rs +++ b/cli/tests/test_init_command.rs @@ -74,6 +74,8 @@ fn test_init_git_internal() { let (stdout, stderr) = test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "repo" "###); @@ -113,6 +115,8 @@ fn test_init_git_external(bare: bool) { Working copy now at: sqpuoqvx f6950fc1 (empty) (no description set) Parent commit : mwrttmos 8d698d4a my-branch | My commit message Added 1 files, modified 0 files, removed 0 files + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "repo" "###); } @@ -183,6 +187,8 @@ fn test_init_git_colocated() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Done importing changes from the underlying Git repo. + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "." "###); @@ -231,6 +237,8 @@ fn test_init_git_colocated_gitlink() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Done importing changes from the underlying Git repo. + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "." "###); insta::assert_snapshot!(read_git_target(&workspace_root), @"../../../.git"); @@ -267,6 +275,8 @@ fn test_init_git_colocated_symlink_directory() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Done importing changes from the underlying Git repo. + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "." "###); insta::assert_snapshot!(read_git_target(&workspace_root), @"../../../.git"); @@ -306,6 +316,8 @@ fn test_init_git_colocated_symlink_directory_without_bare_config() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Done importing changes from the underlying Git repo. + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "." "###); insta::assert_snapshot!(read_git_target(&workspace_root), @"../../../.git"); @@ -347,6 +359,8 @@ fn test_init_git_colocated_symlink_gitlink() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Done importing changes from the underlying Git repo. + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "." "###); insta::assert_snapshot!(read_git_target(&workspace_root), @"../../../.git"); @@ -407,6 +421,8 @@ fn test_init_git_colocated_imported_refs() { let (_stdout, stderr) = test_env.jj_cmd_ok(&local_path, &["init", "--git-repo=."]); insta::assert_snapshot!(stderr, @r###" Done importing changes from the underlying Git repo. + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "." "###); insta::assert_snapshot!(get_branch_output(&test_env, &local_path), @r###" @@ -428,6 +444,8 @@ fn test_init_git_colocated_imported_refs() { The following remote branches aren't associated with the existing local branches: local-remote@origin Hint: Run `jj branch track local-remote@origin` to keep local branches updated on future pulls. + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "." "###); insta::assert_snapshot!(get_branch_output(&test_env, &local_path), @r###" @@ -451,6 +469,8 @@ fn test_init_git_external_but_git_dir_exists() { ); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" + warning: `--git` and `--git-repo` are deprecated. + Use `jj git init` to create git repos Initialized repo in "." "###); @@ -479,7 +499,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 init --git-repo=.` instead. + Hint: To create a repo backed by the existing Git repo, run `jj git init --git-repo=.` instead. "###); } @@ -497,7 +517,7 @@ fn test_init_local_disallowed() { let stdout = test_env.jj_cmd_failure(test_env.env_root(), &["init", "repo"]); insta::assert_snapshot!(stdout, @r###" Error: The native backend is disallowed by default. - Hint: Did you mean to pass `--git`? + Hint: Did you mean to call `jj git init`? Set `ui.allow-init-native` to allow initializing a repo with the native backend. "###); }