mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
git_ui: Search commits by hash (#59132)
## Summary - Allows Git Graph search to match abbreviated or full commit hashes when the query looks like a SHA. - Keeps the existing message search behavior for non-hash queries. - Mirrors the hash search heuristic in the fake git backend and adds GPUI coverage for hash and message search. <img width="1912" height="1241" alt="image" src="https://github.com/user-attachments/assets/903b438e-baa8-4447-95dc-faf321bca6a5" /> ## Test Plan - `cargo fmt --check --package git_ui` - `cargo -q test -p git_ui test_git_graph_search_matches_commit_hash_prefix -- --nocapture` - `./script/clippy -p git_ui` ## Suggested .rules additions - N/A Release Notes: - Improved Git Graph search to find commits by abbreviated or full hash.
This commit is contained in:
parent
13dd39b4e4
commit
356e396517
3 changed files with 162 additions and 9 deletions
|
|
@ -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<Oid>,
|
||||
) -> 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::<Vec<_>>()
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue