From 66aa289838d837e58a71bfdd942186dbb354e1fe Mon Sep 17 00:00:00 2001 From: gaojunran Date: Tue, 7 Jul 2026 12:17:13 +0000 Subject: [PATCH 1/5] Fix Git panel history in detached HEAD --- crates/git_ui/src/git_panel.rs | 63 ++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index b1667adff5972e..cafe6e8b544a0e 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -5807,12 +5807,17 @@ impl GitPanel { return; }; - let Some(branch) = active_repository.read(cx).branch.as_ref() else { + let log_source = { + let repository = active_repository.read(cx); + Self::commit_history_log_source( + repository.branch.as_ref(), + repository.head_commit.as_ref(), + ) + }; + let Some(log_source) = log_source else { return; }; - let branch_name = branch.name().to_string(); - let log_source = LogSource::Branch(branch_name.into()); let log_order = LogOrder::DateOrder; // Kick off the git log fetch so data is ready when the user switches to History. @@ -5852,12 +5857,18 @@ impl GitPanel { return; }; - let Some(branch) = active_repository.read(cx).branch.as_ref() else { + let log_source = { + let repository = active_repository.read(cx); + Self::commit_history_log_source( + repository.branch.as_ref(), + repository.head_commit.as_ref(), + ) + }; + let Some(log_source) = log_source else { + self.commit_history_shas = Some(Vec::new()); return; }; - let branch_name = branch.name().to_string(); - let log_source = LogSource::Branch(branch_name.into()); let log_order = LogOrder::DateOrder; self.commit_history_shas = Some(active_repository.update(cx, |repository, cx| { @@ -5866,6 +5877,25 @@ impl GitPanel { })); } + fn commit_history_log_source( + branch: Option<&Branch>, + head_commit: Option<&CommitDetails>, + ) -> Option { + if let Some(branch) = branch { + return Some(LogSource::Branch(branch.name().to_string().into())); + } + + let sha = match head_commit?.sha.as_ref().parse::() { + Ok(sha) => sha, + Err(error) => { + log::warn!("failed to parse HEAD commit sha for commit history: {error}"); + return None; + } + }; + + Some(LogSource::Sha(sha)) + } + fn git_remote(&self, cx: &mut App) -> Option { let repo = self.active_repository.as_ref()?; let remote_url = repo.read(cx).default_remote_url()?; @@ -8084,7 +8114,7 @@ pub(crate) fn commit_title_exceeds_limit(title: &str, max_length: usize) -> bool #[cfg(test)] mod tests { use git::{ - repository::repo_path, + repository::{CommitDetails, repo_path}, status::{StatusCode, UnmergedStatus, UnmergedStatusCode}, }; use gpui::{TestAppContext, UpdateGlobal, VisualTestContext, px}; @@ -8196,6 +8226,25 @@ mod tests { handle.await; } + #[test] + fn test_commit_history_log_source_uses_head_commit_when_detached() { + let sha = "0123456789abcdef0123456789abcdef01234567"; + let source = GitPanel::commit_history_log_source( + None, + Some(&CommitDetails { + sha: sha.into(), + ..Default::default() + }), + ); + + assert_eq!(source, Some(LogSource::Sha(sha.parse().unwrap()))); + } + + #[test] + fn test_commit_history_log_source_is_empty_without_branch_or_head() { + assert_eq!(GitPanel::commit_history_log_source(None, None), None); + } + fn assert_editor_opened_with_path( workspace: &Entity, expected_path: &Path, From 7d312b8e2ba53571cc9523d154247d81311a21a8 Mon Sep 17 00:00:00 2001 From: gaojunran Date: Tue, 7 Jul 2026 14:35:52 +0000 Subject: [PATCH 2/5] Track Git panel history loading state --- crates/git_ui/src/git_panel.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index cafe6e8b544a0e..153f9935d983bb 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -58,7 +58,8 @@ use project::git_store::GitAccess; use project::{ Fs, Project, ProjectPath, git_store::{ - CommitDataState, GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op, + CommitDataState, GitGraphEvent, GitStoreEvent, Repository, RepositoryEvent, RepositoryId, + pending_op, }, project_settings::{GitPathStyle, ProjectSettings}, }; @@ -805,6 +806,7 @@ pub struct GitPanel { active_tab: GitPanelTab, commit_history_scroll_handle: UniformListScrollHandle, commit_history_shas: Option>, + commit_history_loading: bool, focused_history_entry: Option, history_keyboard_nav: bool, _commit_message_buffer_subscription: Option, @@ -1088,6 +1090,7 @@ impl GitPanel { active_tab: GitPanelTab::Changes, commit_history_scroll_handle: UniformListScrollHandle::new(), commit_history_shas: None, + commit_history_loading: false, focused_history_entry: None, history_keyboard_nav: false, _commit_message_buffer_subscription: None, @@ -5681,7 +5684,9 @@ impl GitPanel { .commit_history_shas .as_ref() .map_or(false, |shas| !shas.is_empty()); - let is_loading = self.commit_history_shas.is_none() && has_repo; + let is_loading = has_repo + && !has_commits + && (self.commit_history_shas.is_none() || self.commit_history_loading); if is_loading { this.child( h_flex() @@ -5795,6 +5800,7 @@ impl GitPanel { GitPanelTab::Changes => { self.focus_handle.focus(window, cx); self.commit_history_shas.take(); + self.commit_history_loading = false; self.focused_history_entry = None; self._repo_subscriptions.clear(); } @@ -5836,9 +5842,15 @@ impl GitPanel { self._repo_subscriptions.push(cx.subscribe( active_repository, |this, _repo, event, cx| { - if let RepositoryEvent::GraphEvent(_, _) = event { + if let RepositoryEvent::GraphEvent(_, event) = event { if this.active_tab == GitPanelTab::History { this.fetch_commit_history_shas(cx); + if matches!( + event, + GitGraphEvent::FullyLoaded | GitGraphEvent::LoadingError + ) { + this.commit_history_loading = false; + } } } }, @@ -5866,15 +5878,21 @@ impl GitPanel { }; let Some(log_source) = log_source else { self.commit_history_shas = Some(Vec::new()); + self.commit_history_loading = false; return; }; let log_order = LogOrder::DateOrder; - self.commit_history_shas = Some(active_repository.update(cx, |repository, cx| { + let (shas, is_loading) = active_repository.update(cx, |repository, cx| { let response = repository.graph_data(log_source, log_order, 0..usize::MAX, cx); - response.commits.iter().map(|commit| commit.sha).collect() - })); + ( + response.commits.iter().map(|commit| commit.sha).collect(), + response.is_loading, + ) + }); + self.commit_history_shas = Some(shas); + self.commit_history_loading = is_loading; } fn commit_history_log_source( From b8b89585b03fc9b7290be70d6606321c299b3b54 Mon Sep 17 00:00:00 2001 From: gaojunran Date: Tue, 7 Jul 2026 14:46:10 +0000 Subject: [PATCH 3/5] Simplify Git panel history loading state --- crates/git_ui/src/git_panel.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 227f01a958c0b8..938b60f880fc28 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -807,7 +807,6 @@ pub struct GitPanel { active_tab: GitPanelTab, commit_history_scroll_handle: UniformListScrollHandle, commit_history_entries: Option>, - commit_history_loading: bool, focused_history_entry: Option, history_keyboard_nav: bool, _commit_message_buffer_subscription: Option, @@ -1110,7 +1109,6 @@ impl GitPanel { active_tab: GitPanelTab::Changes, commit_history_scroll_handle: UniformListScrollHandle::new(), commit_history_entries: None, - commit_history_loading: false, focused_history_entry: None, history_keyboard_nav: false, _commit_message_buffer_subscription: None, @@ -5704,9 +5702,7 @@ impl GitPanel { .commit_history_entries .as_ref() .map_or(false, |entries| !entries.is_empty()); - let is_loading = has_repo - && !has_commits - && (self.commit_history_entries.is_none() || self.commit_history_loading); + let is_loading = self.commit_history_entries.is_none() && has_repo; if is_loading { this.child( h_flex() @@ -5830,7 +5826,6 @@ impl GitPanel { GitPanelTab::Changes => { self.focus_handle.focus(window, cx); self.commit_history_entries.take(); - self.commit_history_loading = false; self.focused_history_entry = None; self._repo_subscriptions.clear(); } @@ -5902,7 +5897,6 @@ impl GitPanel { }; let Some(log_source) = log_source else { self.commit_history_entries = Some(Rc::from([])); - self.commit_history_loading = false; return; }; @@ -5919,8 +5913,11 @@ impl GitPanel { response.is_loading, ) }); - self.commit_history_entries = Some(entries); - self.commit_history_loading = is_loading; + self.commit_history_entries = if is_loading && entries.is_empty() { + None + } else { + Some(entries) + }; } fn commit_history_log_source( From 00111bd5b7828e341afbe94a57e59ece5ccccca1 Mon Sep 17 00:00:00 2001 From: gaojunran Date: Tue, 7 Jul 2026 15:54:12 +0000 Subject: [PATCH 4/5] Fix commit history entry type inference --- crates/git_ui/src/git_panel.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 938b60f880fc28..59af5e16c26c27 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -5902,17 +5902,18 @@ impl GitPanel { let log_order = LogOrder::DateOrder; - let (entries, is_loading) = active_repository.update(cx, |repository, cx| { - let response = repository.graph_data(log_source, log_order, 0..usize::MAX, cx); - ( - response - .commits - .iter() - .map(CommitHistoryEntry::from) - .collect(), - response.is_loading, - ) - }); + let (entries, is_loading): (Rc<[CommitHistoryEntry]>, bool) = + active_repository.update(cx, |repository, cx| { + let response = repository.graph_data(log_source, log_order, 0..usize::MAX, cx); + ( + response + .commits + .iter() + .map(CommitHistoryEntry::from) + .collect(), + response.is_loading, + ) + }); self.commit_history_entries = if is_loading && entries.is_empty() { None } else { From 1dcacaedb26ad6f4e527cfb44c29f385855f074e Mon Sep 17 00:00:00 2001 From: gaojunran Date: Wed, 8 Jul 2026 01:47:23 +0000 Subject: [PATCH 5/5] Fix git log source SHA arguments --- crates/git/src/repository.rs | 60 +++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 6c88ee4e688336..192ea802fb1173 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -757,20 +757,22 @@ pub enum LogSource { } impl LogSource { - fn get_args(&self) -> Result> { + fn get_args(&self) -> Vec { match self { - LogSource::All => Ok(vec![ - "--ignore-missing", // needed in case of unborn HEAD - "--branches", - "--remotes", - "--tags", - "HEAD", - ]), - LogSource::Branch(branch) => Ok(vec![branch.as_str()]), - LogSource::Sha(oid) => Ok(vec![ - str::from_utf8(oid.as_bytes()).context("Failed to build str from sha")?, - ]), - LogSource::Path(path) => Ok(vec!["--follow", "--", path.as_unix_str()]), + LogSource::All => vec![ + OsString::from("--ignore-missing"), // needed in case of unborn HEAD + OsString::from("--branches"), + OsString::from("--remotes"), + OsString::from("--tags"), + OsString::from("HEAD"), + ], + LogSource::Branch(branch) => vec![OsString::from(branch.as_ref())], + LogSource::Sha(oid) => vec![OsString::from(oid.to_string())], + LogSource::Path(path) => vec![ + OsString::from("--follow"), + OsString::from("--"), + OsString::from(path.as_unix_str()), + ], } } } @@ -3111,8 +3113,12 @@ impl GitRepository for RealGitRepository { let git = self.git_binary(); async move { - let mut git_log_command = vec!["log", GRAPH_COMMIT_FORMAT, log_order.as_arg()]; - git_log_command.extend(log_source.get_args()?); + let mut git_log_command = vec![ + OsString::from("log"), + OsString::from(GRAPH_COMMIT_FORMAT), + OsString::from(log_order.as_arg()), + ]; + git_log_command.extend(log_source.get_args()); let mut command = git.build_command(&git_log_command); command.stdout(Stdio::piped()); command.stderr(Stdio::piped()); @@ -3182,22 +3188,22 @@ impl GitRepository for RealGitRepository { let git = self.git_binary(); async move { - let mut args = vec!["log", SEARCH_COMMIT_FORMAT]; + let mut args = vec![OsString::from("log"), OsString::from(SEARCH_COMMIT_FORMAT)]; let hash_query = commit_hash_search_query(search_args.query.as_str()) .map(|query| query.to_ascii_lowercase()); if hash_query.is_none() { - args.push("--fixed-strings"); + args.push(OsString::from("--fixed-strings")); if !search_args.case_sensitive { - args.push("--regexp-ignore-case"); + args.push(OsString::from("--regexp-ignore-case")); } - args.push("--grep"); - args.push(search_args.query.as_str()); + args.push(OsString::from("--grep")); + args.push(OsString::from(search_args.query.as_ref())); } - args.extend(log_source.get_args()?); + args.extend(log_source.get_args()); let mut command = git.build_command(&args); command.stdout(Stdio::piped()); command.stderr(Stdio::null()); @@ -3970,6 +3976,18 @@ mod tests { git_command(path, ["init", "-b", "main"]); } + #[test] + fn test_log_source_sha_uses_hex_arg() { + let sha = "0123456789abcdef0123456789abcdef01234567"; + let args = LogSource::Sha(sha.parse().unwrap()) + .get_args() + .into_iter() + .map(|arg| arg.into_string().unwrap()) + .collect::>(); + + assert_eq!(args, vec![sha]); + } + fn clone_remote_repository_with_main_and_feature(temp_dir: &Path) -> (PathBuf, PathBuf) { let remote_directory = temp_dir.join("remote.git"); let seed_directory = temp_dir.join("seed");