diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 5d8fc647fe0..45ee57b35f5 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -15,6 +15,7 @@ use git::{ CreateWorktreeTarget, FetchOptions, FileHistoryChangedFileSets, GRAPH_CHUNK_SIZE, GitRepository, GitRepositoryCheckpoint, InitialGraphCommitData, LogOrder, LogSource, PushOptions, RefEdit, Remote, RepoPath, ResetMode, SearchCommitArgs, Worktree, + commit_hash_search_query, }, stash::GitStash, status::{ @@ -1491,7 +1492,9 @@ impl GitRepository for FakeGitRepository { request_tx: Sender, ) -> BoxFuture<'_, Result<()>> { async move { - let query = if search_args.case_sensitive { + let hash_query = commit_hash_search_query(search_args.query.as_str()) + .map(|query| query.to_ascii_lowercase()); + let message_query = if search_args.case_sensitive { search_args.query.to_string() } else { search_args.query.to_lowercase() @@ -1505,12 +1508,19 @@ impl GitRepository for FakeGitRepository { let FakeCommitDataEntry::Success(commit_data) = entry else { return None; }; + if let Some(hash_query) = hash_query.as_ref() { + return sha + .to_string() + .to_ascii_lowercase() + .starts_with(hash_query) + .then_some(*sha); + } let message = if search_args.case_sensitive { commit_data.message.to_string() } else { commit_data.message.to_lowercase() }; - message.contains(&query).then_some(*sha) + message.contains(&message_query).then_some(*sha) }) .collect::>() })?; diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index f077390375b..93338de75cc 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -780,6 +780,14 @@ pub struct SearchCommitArgs { pub case_sensitive: bool, } +pub fn commit_hash_search_query(query: &str) -> Option<&str> { + let query = query.trim(); + (7..=40) + .contains(&query.len()) + .then_some(query) + .filter(|query| query.bytes().all(|byte| byte.is_ascii_hexdigit())) +} + pub fn delete_branch_flag(is_remote_tracking_ref: bool, force: bool) -> &'static str { match (is_remote_tracking_ref, force) { (true, true) => "-Dr", @@ -3139,16 +3147,20 @@ impl GitRepository for RealGitRepository { async move { let mut args = vec!["log", SEARCH_COMMIT_FORMAT]; + let hash_query = commit_hash_search_query(search_args.query.as_str()) + .map(|query| query.to_ascii_lowercase()); - args.push("--fixed-strings"); + if hash_query.is_none() { + args.push("--fixed-strings"); - if !search_args.case_sensitive { - args.push("--regexp-ignore-case"); + if !search_args.case_sensitive { + args.push("--regexp-ignore-case"); + } + + args.push("--grep"); + args.push(search_args.query.as_str()); } - args.push("--grep"); - args.push(search_args.query.as_str()); - args.extend(log_source.get_args()?); let mut command = git.build_command(&args); command.stdout(Stdio::piped()); @@ -3169,6 +3181,11 @@ impl GitRepository for RealGitRepository { } let sha = line_buffer.trim_end_matches('\n'); + if let Some(hash_query) = hash_query.as_ref() + && !sha.to_ascii_lowercase().starts_with(hash_query) + { + continue; + } if let Ok(oid) = Oid::from_str(sha) && request_tx.send(oid).await.is_err() diff --git a/crates/git_ui/src/git_graph.rs b/crates/git_ui/src/git_graph.rs index 36b63401c43..9422db54d82 100644 --- a/crates/git_ui/src/git_graph.rs +++ b/crates/git_ui/src/git_graph.rs @@ -4624,7 +4624,7 @@ mod tests { use collections::{HashMap, HashSet}; use fs::FakeFs; use git::Oid; - use git::repository::InitialGraphCommitData; + use git::repository::{CommitData, InitialGraphCommitData}; use gpui::{TestAppContext, UpdateGlobal}; use project::git_store::{GitStoreEvent, RepositoryEvent}; use project::{Project, TaskSourceKind, task_store::TaskSettingsLocation}; @@ -5973,6 +5973,132 @@ mod tests { }); } + #[gpui::test] + async fn test_git_graph_search_matches_commit_hash_prefix(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + Path::new("/project"), + json!({ + ".git": {}, + "file.txt": "content", + }), + ) + .await; + + let first_sha = Oid::from_bytes(&[1; 20]).unwrap(); + let target_sha = Oid::from_bytes(&[2; 20]).unwrap(); + let third_sha = Oid::from_bytes(&[3; 20]).unwrap(); + let commits = vec![ + Arc::new(InitialGraphCommitData { + sha: first_sha, + parents: smallvec![target_sha], + ref_names: vec!["HEAD".into(), "refs/heads/main".into()], + }), + Arc::new(InitialGraphCommitData { + sha: target_sha, + parents: smallvec![third_sha], + ref_names: vec![], + }), + Arc::new(InitialGraphCommitData { + sha: third_sha, + parents: smallvec![], + ref_names: vec![], + }), + ]; + fs.set_graph_commits(Path::new("/project/.git"), commits); + fs.set_commit_data( + Path::new("/project/.git"), + [ + ( + CommitData { + sha: first_sha, + parents: smallvec![target_sha], + author_name: "Author".into(), + author_email: "author@example.com".into(), + commit_timestamp: 1, + subject: "Add feature".into(), + message: "Add feature".into(), + }, + false, + ), + ( + CommitData { + sha: target_sha, + parents: smallvec![third_sha], + author_name: "Author".into(), + author_email: "author@example.com".into(), + commit_timestamp: 2, + subject: "Fix branch loading".into(), + message: "Fix branch loading".into(), + }, + false, + ), + ( + CommitData { + sha: third_sha, + parents: smallvec![], + author_name: "Author".into(), + author_email: "author@example.com".into(), + commit_timestamp: 3, + subject: "Update docs".into(), + message: "Update docs".into(), + }, + false, + ), + ], + ); + + let project = Project::test(fs.clone(), [Path::new("/project")], cx).await; + cx.run_until_parked(); + + let repository = project.read_with(cx, |project, cx| { + project + .active_repository(cx) + .expect("should have a repository") + }); + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + workspace::MultiWorkspace::test_new(project.clone(), window, cx) + }); + let workspace_weak = + multi_workspace.read_with(&*cx, |multi, _| multi.workspace().downgrade()); + let git_graph = cx.new_window_entity(|window, cx| { + GitGraph::new( + repository.read(cx).id, + project.read(cx).git_store().clone(), + workspace_weak, + None, + window, + cx, + ) + }); + cx.run_until_parked(); + + git_graph.update(cx, |graph, cx| { + graph.search_for_test("0202020".into(), cx); + }); + cx.run_until_parked(); + + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.search_matches_for_test(), vec![target_sha]); + let selected_sha = graph + .selected_entry_idx + .and_then(|idx| graph.graph_data.commits.get(idx)) + .map(|commit| commit.data.sha); + assert_eq!(selected_sha, Some(target_sha)); + }); + + git_graph.update(cx, |graph, cx| { + graph.search_for_test("docs".into(), cx); + }); + cx.run_until_parked(); + + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.search_matches_for_test(), vec![third_sha]); + }); + } + #[gpui::test] async fn test_graph_data_reloaded_after_stash_change(cx: &mut TestAppContext) { init_test(cx);