cli: do initial import of Git refs without touching HEAD

This reimplements the change 9faa4670d5 "cli: on init, import git refs prior
to importing HEAD." Initialization is special because the HEAD ref isn't
available to jj yet, and there is an empty working-copy commit.

The initialization function could be refactored to go through the common code
path, but I think doing that would make future improvement harder. We might
want to initialize tracking branches based on .git/config for example.

Fixes #2942
This commit is contained in:
Yuya Nishihara 2024-02-05 11:49:20 +09:00
parent 5118d01385
commit 1adf6b5d6e
3 changed files with 116 additions and 8 deletions

View file

@ -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.

View file

@ -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<ReadonlyRepo>,
colocated: bool,
) -> Result<Arc<ReadonlyRepo>, 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,

View file

@ -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();