From 5daadc0d30c81829c87fd0c9b26badcecfb6c8ee Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 5 Mar 2025 17:31:45 -0500 Subject: [PATCH] git: Add CHERRY_PICK_HEAD to the list of merge heads (#26145) Attempt to fix an issue where conflicts from a cherry-pick don't get cleared out of the git panel after being resolved. Release Notes: - Git Beta: Fixed resolution of conflicts from cherry-picks not being reflected in the git panel --- crates/git/src/repository.rs | 9 +++ crates/worktree/src/worktree_tests.rs | 103 ++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index b7ef907b57..1bf47ac670 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -427,6 +427,15 @@ impl GitRepository for RealGitRepository { true }) .ok(); + if let Some(oid) = self + .repository + .lock() + .find_reference("CHERRY_PICK_HEAD") + .ok() + .and_then(|reference| reference.target()) + { + shas.push(oid.to_string()) + } shas } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index d889cc71cb..53a74db398 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -5,6 +5,7 @@ use crate::{ use anyhow::Result; use fs::{FakeFs, Fs, RealFs, RemoveOptions}; use git::{ + repository::RepoPath, status::{ FileStatus, GitSummary, StatusCode, TrackedStatus, TrackedSummary, UnmergedStatus, UnmergedStatusCode, @@ -3307,6 +3308,87 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let root = TempTree::new(json!({ + "project": { + "a.txt": "a", + }, + })); + let root_path = root.path(); + + let tree = Worktree::local( + root_path, + true, + Arc::new(RealFs::default()), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + let repo = git_init(&root_path.join("project")); + git_add("a.txt", &repo); + git_commit("init", &repo); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + tree.flush_fs_events(cx).await; + + git_branch("other-branch", &repo); + git_checkout("refs/heads/other-branch", &repo); + std::fs::write(root_path.join("project/a.txt"), "A").unwrap(); + git_add("a.txt", &repo); + git_commit("capitalize", &repo); + let commit = repo + .head() + .expect("Failed to get HEAD") + .peel_to_commit() + .expect("HEAD is not a commit"); + git_checkout("refs/heads/master", &repo); + std::fs::write(root_path.join("project/a.txt"), "b").unwrap(); + git_add("a.txt", &repo); + git_commit("improve letter", &repo); + git_cherry_pick(&commit, &repo); + std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD")) + .expect("No CHERRY_PICK_HEAD"); + pretty_assertions::assert_eq!( + git_status(&repo), + collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)]) + ); + tree.flush_fs_events(cx).await; + let conflicts = tree.update(cx, |tree, _| { + let entry = tree.git_entries().nth(0).expect("No git entry").clone(); + entry + .current_merge_conflicts + .iter() + .cloned() + .collect::>() + }); + pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]); + + git_add("a.txt", &repo); + // Attempt to manually simulate what `git cherry-pick --continue` would do. + git_commit("whatevs", &repo); + std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD")) + .expect("Failed to remove CHERRY_PICK_HEAD"); + pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default()); + tree.flush_fs_events(cx).await; + let conflicts = tree.update(cx, |tree, _| { + let entry = tree.git_entries().nth(0).expect("No git entry").clone(); + entry + .current_merge_conflicts + .iter() + .cloned() + .collect::>() + }); + pretty_assertions::assert_eq!(conflicts, []); +} + #[gpui::test] async fn test_private_single_file_worktree(cx: &mut TestAppContext) { init_test(cx); @@ -3405,6 +3487,11 @@ fn git_commit(msg: &'static str, repo: &git2::Repository) { } } +#[track_caller] +fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) { + repo.cherrypick(commit, None).expect("Failed to cherrypick"); +} + #[track_caller] fn git_stash(repo: &mut git2::Repository) { use git2::Signature; @@ -3430,6 +3517,22 @@ fn git_reset(offset: usize, repo: &git2::Repository) { .expect("Could not reset"); } +#[track_caller] +fn git_branch(name: &str, repo: &git2::Repository) { + let head = repo + .head() + .expect("Couldn't get repo head") + .peel_to_commit() + .expect("HEAD is not a commit"); + repo.branch(name, &head, false).expect("Failed to commit"); +} + +#[track_caller] +fn git_checkout(name: &str, repo: &git2::Repository) { + repo.set_head(name).expect("Failed to set head"); + repo.checkout_head(None).expect("Failed to check out head"); +} + #[allow(dead_code)] #[track_caller] fn git_status(repo: &git2::Repository) -> collections::HashMap {