diff --git a/lib/src/git.rs b/lib/src/git.rs index 9254ec10f0..4dbe7a38f4 100644 --- a/lib/src/git.rs +++ b/lib/src/git.rs @@ -933,7 +933,21 @@ pub fn reset_head( let git_head = mut_repo.view().git_head(); let new_git_commit_id = Oid::from_bytes(first_parent_id.as_bytes()).unwrap(); let new_git_commit = git_repo.find_commit(new_git_commit_id)?; - if git_head != &first_parent { + if git_head == &first_parent { + // `HEAD@git` already points to the correct commit, so we only need to reset + // the Git index. Only do so if it is non-empty (i.e. a user used `git add`). + // In large repositories, this is around 2x faster if the Git index is empty + // (~0.89s to check the diff, vs. ~1.72s to reset), and around 8% slower if + // it isn't (~1.86s to check the diff AND reset). + let diff = git_repo.diff_tree_to_index( + Some(&new_git_commit.tree()?), + None, + Some(git2::DiffOptions::new().skip_binary_check(true)), + )?; + if diff.deltas().len() == 0 { + return Ok(()); + } + } else { git_repo.set_head_detached(new_git_commit_id)?; mut_repo.set_git_head_target(first_parent); } diff --git a/lib/tests/test_git.rs b/lib/tests/test_git.rs index 546b751a2f..802a3c83c6 100644 --- a/lib/tests/test_git.rs +++ b/lib/tests/test_git.rs @@ -13,7 +13,7 @@ // limitations under the License. use std::collections::{BTreeMap, HashSet}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{mpsc, Arc, Barrier}; use std::{fs, iter, thread}; @@ -33,6 +33,7 @@ use jj_lib::object_id::ObjectId; use jj_lib::op_store::{BranchTarget, RefTarget, RemoteRef, RemoteRefState}; use jj_lib::refs::BranchPushUpdate; use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo}; +use jj_lib::repo_path::RepoPath; use jj_lib::settings::{GitSettings, UserSettings}; use jj_lib::signing::Signer; use jj_lib::str_util::StringPattern; @@ -2119,6 +2120,50 @@ fn test_reset_head_to_root() { assert!(git_repo.find_reference("refs/jj/root").is_err()); } +#[test] +fn test_reset_head_with_index() { + // Create colocated workspace + let settings = testutils::user_settings(); + let temp_dir = testutils::new_temp_dir(); + let workspace_root = temp_dir.path().join("repo"); + let git_repo = git2::Repository::init(&workspace_root).unwrap(); + let (_workspace, repo) = + Workspace::init_external_git(&settings, &workspace_root, &workspace_root.join(".git")) + .unwrap(); + + let mut tx = repo.start_transaction(&settings); + let mut_repo = tx.mut_repo(); + + let root_commit_id = repo.store().root_commit_id(); + let tree_id = repo.store().empty_merged_tree_id(); + let commit1 = mut_repo + .new_commit(&settings, vec![root_commit_id.clone()], tree_id.clone()) + .write() + .unwrap(); + let commit2 = mut_repo + .new_commit(&settings, vec![commit1.id().clone()], tree_id.clone()) + .write() + .unwrap(); + + // Set Git HEAD to commit2's parent (i.e. commit1) + git::reset_head(tx.mut_repo(), &git_repo, &commit2).unwrap(); + assert!(git_repo.index().unwrap().is_empty()); + + // Add "staged changes" to the Git index + let file_path = RepoPath::from_internal_string("file.txt"); + testutils::write_working_copy_file(&workspace_root, file_path, "i am a file\n"); + git_repo + .index() + .unwrap() + .add_path(&file_path.to_fs_path(Path::new(""))) + .unwrap(); + assert!(!git_repo.index().unwrap().is_empty()); + + // Reset head to and the Git index + git::reset_head(tx.mut_repo(), &git_repo, &commit2).unwrap(); + assert!(git_repo.index().unwrap().is_empty()); +} + #[test] fn test_init() { let settings = testutils::user_settings();