diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 3ddff9761..603b50b9f 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -951,9 +951,10 @@ impl WorkspaceCommandHelper { /// If the working-copy branch is rebased, and if update is allowed, the new /// working-copy commit will be checked out. /// - /// This function does not import the Git HEAD. + /// This function does not import the Git HEAD, but the HEAD may be reset to + /// the working copy parent if the repository is colocated. #[instrument(skip_all)] - pub fn import_git_refs(&mut self, ui: &mut Ui) -> Result<(), CommandError> { + fn import_git_refs(&mut self, ui: &mut Ui) -> Result<(), CommandError> { let git_settings = self.settings.git_settings(); let mut tx = self.start_transaction(); // Automated import shouldn't fail because of reserved remote name. diff --git a/cli/src/commands/git.rs b/cli/src/commands/git.rs index b426b44a4..534de8085 100644 --- a/cli/src/commands/git.rs +++ b/cli/src/commands/git.rs @@ -16,6 +16,7 @@ use std::collections::HashSet; use std::io::Write; use std::ops::Deref; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::{fmt, fs, io}; use clap::{ArgGroup, Subcommand}; @@ -30,7 +31,7 @@ use jj_lib::op_store::RefTarget; use jj_lib::refs::{ classify_branch_push_action, BranchPushAction, BranchPushUpdate, TrackingRefPair, }; -use jj_lib::repo::Repo; +use jj_lib::repo::{ReadonlyRepo, Repo}; use jj_lib::repo_path::RepoPath; use jj_lib::revset::{self, RevsetExpression, RevsetIteratorExt as _}; use jj_lib::settings::{ConfigResultExt as _, UserSettings}; @@ -41,12 +42,13 @@ use maplit::hashset; use crate::cli_util::{ parse_string_pattern, print_trackable_remote_branches, resolve_multiple_nonempty_revsets, - short_change_hash, short_commit_hash, user_error, user_error_with_hint, + short_change_hash, short_commit_hash, start_repo_transaction, user_error, user_error_with_hint, user_error_with_hint_opt, user_error_with_message, CommandError, CommandHelper, RevisionArg, WorkspaceCommandHelper, }; use crate::git_util::{ - get_git_repo, print_failed_git_export, print_git_import_stats, with_remote_git_callbacks, + get_git_repo, is_colocated_git_workspace, print_failed_git_export, print_git_import_stats, + with_remote_git_callbacks, }; use crate::ui::Ui; @@ -388,11 +390,12 @@ pub fn git_init( let git_store_path = cwd.join(git_store_str); let (workspace, repo) = Workspace::init_external_git(command.settings(), workspace_root, &git_store_path)?; - let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?; - maybe_add_gitignore(&workspace_command)?; // Import refs first so all the reachable commits are indexed in // chronological order. - workspace_command.import_git_refs(ui)?; + 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(); @@ -423,6 +426,45 @@ pub fn git_init( Ok(()) } +/// Imports branches and tags from the underlying Git repo, exports changes if +/// the repo is colocated. +/// +/// This is similar to `WorkspaceCommandHelper::import_git_refs()`, but never +/// moves the Git HEAD to the working copy parent. +fn init_git_refs( + ui: &mut Ui, + command: &CommandHelper, + repo: Arc, + colocated: bool, +) -> Result, CommandError> { + let mut tx = start_repo_transaction(&repo, command.settings(), command.string_args()); + // There should be no old refs to abandon, but enforce it. + let mut git_settings = command.settings().git_settings(); + git_settings.abandon_unreachable_commits = false; + let stats = git::import_some_refs( + tx.mut_repo(), + &git_settings, + // Initial import shouldn't fail because of reserved remote name. + |ref_name| !git::is_reserved_git_remote_ref(ref_name), + )?; + if !tx.mut_repo().has_changes() { + return Ok(repo); + } + print_git_import_stats(ui, &stats)?; + if colocated { + // If git.auto-local-branch = true, local branches could be created for + // the imported remote branches. + let failed_branches = git::export_refs(tx.mut_repo())?; + print_failed_git_export(ui, &failed_branches)?; + } + let repo = tx.commit("import git refs"); + writeln!( + ui.stderr(), + "Done importing changes from the underlying Git repo." + )?; + Ok(repo) +} + fn cmd_git_init( ui: &mut Ui, command: &CommandHelper, diff --git a/cli/tests/test_git_init.rs b/cli/tests/test_git_init.rs index 97d47b65e..c9ae09cf2 100644 --- a/cli/tests/test_git_init.rs +++ b/cli/tests/test_git_init.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Write as _; use std::path::{Path, PathBuf}; use test_case::test_case; @@ -441,6 +442,70 @@ fn test_git_init_colocated_via_git_repo_path_imported_refs() { "###); } +#[test] +fn test_git_init_colocated_dirty_working_copy() { + let test_env = TestEnvironment::default(); + let workspace_root = test_env.env_root().join("repo"); + let git_repo = init_git_repo(&workspace_root, false); + + let add_file_to_index = |name: &str, data: &str| { + std::fs::write(workspace_root.join(name), data).unwrap(); + let mut index = git_repo.index().unwrap(); + index.add_path(Path::new(name)).unwrap(); + index.write().unwrap(); + }; + let get_git_statuses = || { + let mut buf = String::new(); + for entry in git_repo.statuses(None).unwrap().iter() { + writeln!(buf, "{:?} {}", entry.status(), entry.path().unwrap()).unwrap(); + } + buf + }; + + add_file_to_index("some-file", "new content"); + add_file_to_index("new-staged-file", "new content"); + std::fs::write(workspace_root.join("unstaged-file"), "new content").unwrap(); + insta::assert_snapshot!(get_git_statuses(), @r###" + Status(INDEX_NEW) new-staged-file + Status(INDEX_MODIFIED) some-file + Status(WT_NEW) unstaged-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 "." + "###); + + // Working-copy changes should have been snapshotted. + let stdout = test_env.jj_cmd_success(&workspace_root, &["log", "-s", "--ignore-working-copy"]); + insta::assert_snapshot!(stdout, @r###" + @ sqpuoqvx test.user@example.com 2001-02-03 04:05:07.000 +07:00 cd1e144d + │ (no description set) + │ A new-staged-file + │ M some-file + │ A unstaged-file + ◉ mwrttmos git.user@example.com 1970-01-01 01:02:03.000 +01:00 my-branch HEAD@git 8d698d4a + │ My commit message + │ A some-file + ◉ zzzzzzzz root() 00000000 + "###); + + // Git index should be consistent with the working copy parent. With the + // current implementation, the index is unchanged. Since jj created new + // working copy commit, it's also okay to update the index reflecting the + // working copy commit or the working copy parent. + insta::assert_snapshot!(get_git_statuses(), @r###" + Status(IGNORED) .jj/.gitignore + Status(IGNORED) .jj/repo/ + Status(IGNORED) .jj/working_copy/ + Status(INDEX_NEW) new-staged-file + Status(INDEX_MODIFIED) some-file + Status(WT_NEW) unstaged-file + "###); +} + #[test] fn test_git_init_external_but_git_dir_exists() { let test_env = TestEnvironment::default();