Add git blame error reporting with notification (#10408)

Mehmet Efe Akรงa and Thorsten Ball created

<img width="1035" alt="Screenshot 2024-04-11 at 13 13 44"
src="https://github.com/zed-industries/zed/assets/13402668/cd0e96a0-41c6-4757-8840-97d15a75c511">

Release Notes:

- Added a notification to show possible `git blame` errors if it fails to run.

Caveats:
- ~git blame now executes in foreground
executor  (required since the Fut is !Send)~

TODOs:
- After a failed toggle, the app thinks the blame
is shown. This means toggling again will do nothing
instead of retrying. (Caused by editor.show_git_blame
being set to true before the git blame is generated)
- ~(Maybe) Trim error?~ Done

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>

Change summary

crates/editor/src/git/blame.rs | 77 ++++++++++++++++++++++++++++++-----
crates/git/src/blame.rs        |  1 
crates/project/src/project.rs  |  1 
3 files changed, 68 insertions(+), 11 deletions(-)

Detailed changes

crates/editor/src/git/blame.rs ๐Ÿ”—

@@ -256,7 +256,7 @@ impl GitBlame {
         let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx);
 
         self.task = cx.spawn(|this, mut cx| async move {
-            let (entries, permalinks, messages) = cx
+            let result = cx
                 .background_executor()
                 .spawn({
                     let snapshot = snapshot.clone();
@@ -304,16 +304,23 @@ impl GitBlame {
                         anyhow::Ok((entries, permalinks, messages))
                     }
                 })
-                .await?;
-
-            this.update(&mut cx, |this, cx| {
-                this.buffer_edits = buffer_edits;
-                this.buffer_snapshot = snapshot;
-                this.entries = entries;
-                this.permalinks = permalinks;
-                this.messages = messages;
-                this.generated = true;
-                cx.notify();
+                .await;
+
+            this.update(&mut cx, |this, cx| match result {
+                Ok((entries, permalinks, messages)) => {
+                    this.buffer_edits = buffer_edits;
+                    this.buffer_snapshot = snapshot;
+                    this.entries = entries;
+                    this.permalinks = permalinks;
+                    this.messages = messages;
+                    this.generated = true;
+                    cx.notify();
+                }
+                Err(error) => this.project.update(cx, |_, cx| {
+                    log::error!("failed to get git blame data: {error:?}");
+                    let notification = format!("{:#}", error).trim().to_string();
+                    cx.emit(project::Event::Notification(notification));
+                }),
             })
         });
     }
@@ -359,6 +366,54 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_blame_error_notifications(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/my-repo",
+            json!({
+                ".git": {},
+                "file.txt": r#"
+                    irrelevant contents
+                "#
+                .unindent()
+            }),
+        )
+        .await;
+
+        // Creating a GitBlame without a corresponding blame state
+        // will result in an error.
+
+        let project = Project::test(fs, ["/my-repo".as_ref()], cx).await;
+        let buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/my-repo/file.txt", cx)
+            })
+            .await
+            .unwrap();
+
+        let blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project.clone(), cx));
+
+        let event = project.next_event(cx);
+        assert_eq!(
+            event,
+            project::Event::Notification(
+                "Failed to blame \"file.txt\": failed to get blame for \"file.txt\"".to_string()
+            )
+        );
+
+        blame.update(cx, |blame, cx| {
+            assert_eq!(
+                blame
+                    .blame_for_rows((0..1).map(Some), cx)
+                    .collect::<Vec<_>>(),
+                vec![None]
+            );
+        });
+    }
+
     #[gpui::test]
     async fn test_blame_for_rows(cx: &mut gpui::TestAppContext) {
         init_test(cx);

crates/git/src/blame.rs ๐Ÿ”—

@@ -78,6 +78,7 @@ fn run_git_blame(
         .arg(path.as_os_str())
         .stdin(Stdio::piped())
         .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
         .spawn()
         .map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
 

crates/project/src/project.rs ๐Ÿ”—

@@ -7734,6 +7734,7 @@ impl Project {
                 let (repo, relative_path, content) = blame_params?;
                 let lock = repo.lock();
                 lock.blame(&relative_path, content)
+                    .with_context(|| format!("Failed to blame {relative_path:?}"))
             })
         } else {
             let project_id = self.remote_id();