From 5b948e5aaba15cf4446cfdf9cef1cfddcb062bee Mon Sep 17 00:00:00 2001 From: liam Date: Sun, 31 May 2026 19:41:08 -0400 Subject: [PATCH] git: Suppress blame errors outside Git repositories (#56348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves https://github.com/zed-industries/zed/issues/55552 Git blame can be initialized for buffers that are not backed by any Git repository. Previously, that path still called into `Project::blame_buffer`, which returned an error because there was no repository for the buffer. When blame was user-triggered, that error was surfaced as a toast even though this is an expected “not applicable” state rather than a failed Git operation. This diff checks whether the buffer belongs to a repository before requesting blame data. Buffers outside Git repositories now produce no blame entries without emitting an error, while real blame failures inside repositories continue to use the existing error notification path. Release Notes: - Improved Git blame to quietly ignore files that are not part of a Git repository. Co-authored-by: Cole Miller --- crates/editor/src/git/blame.rs | 88 ++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 9ba5c4aa19..a78c37e66d 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -522,14 +522,25 @@ impl GitBlame { let id = buffer.read(cx).remote_id(); let snapshot = buffer.read(cx).snapshot(); let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); - let remote_url = project + + let repository = project .read(cx) .git_store() .read(cx) - .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) + .repository_and_path_for_buffer_id(id, cx); + + let remote_url = repository + .as_ref() .and_then(|(repo, _)| repo.read(cx).default_remote_url()); - let blame_buffer = project - .update(cx, |project, cx| project.blame_buffer(&buffer, None, cx)); + + let blame_buffer = if repository.is_some() { + project.update(cx, |project, cx| { + project.blame_buffer(&buffer, None, cx) + }) + } else { + Task::ready(Ok(None)) + }; + Ok(async move { (id, snapshot, buffer_edits, blame_buffer.await, remote_url) }) @@ -696,7 +707,7 @@ mod tests { use rand::prelude::*; use serde_json::json; use settings::SettingsStore; - use std::{cmp, env, ops::Range, path::Path}; + use std::{cmp, env, ops::Range, path::Path, sync::Mutex}; use text::BufferId; use unindent::Unindent as _; use util::{RandomCharIter, path}; @@ -811,6 +822,73 @@ mod tests { }); } + #[gpui::test] + async fn test_blame_ignores_buffers_outside_git_repositories(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + + fs.insert_tree( + "/not-a-repo", + json!({ + "foo": "bar", + }), + ) + .await; + + let project = Project::test(fs, ["/not-a-repo".as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/not-a-repo/foo", cx) + }) + .await + .unwrap(); + + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); + + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + + let events = Arc::new(Mutex::new(Vec::new())); + + let _subscription = project.update(cx, |_, cx| { + cx.subscribe(&project, { + let events = events.clone(); + + move |_, _, event: &project::Event, _| { + events + .lock() + .expect("events mutex poisoned") + .push(event.clone()); + } + }) + }); + + let blame = cx.new(|cx| GitBlame::new(buffer.clone(), project.clone(), true, true, cx)); + + cx.executor().run_until_parked(); + + assert!(events.lock().expect("events mutex poisoned").is_empty()); + + blame.update(cx, |blame, cx| { + assert_eq!( + blame + .blame_for_rows( + &(0..1) + .map(|row| RowInfo { + buffer_row: Some(row), + buffer_id: Some(buffer_id), + ..Default::default() + }) + .collect::>(), + cx + ) + .collect::>(), + vec![None] + ); + }); + } + #[gpui::test] async fn test_blame_for_rows(cx: &mut gpui::TestAppContext) { init_test(cx);