Clear pending staged/unstaged diff hunks hunks when writing to the git index fails (#26173)

Release Notes:

- Git Beta: Fixed a bug where discarding a hunk in the project diff view
performed two concurrent saves of the buffer.
- Git Beta: Fixed an issue where diff hunks appeared in the wrong state
after failing to write to the git index.
This commit is contained in:
Max Brunsfeld 2025-03-05 18:45:09 -08:00 committed by GitHub
parent d3c68650c0
commit 314ad5dd5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 534 additions and 143 deletions

View file

@ -1,24 +1,38 @@
use crate::buffer_store::BufferStore;
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
use crate::{Project, ProjectPath};
use crate::{
buffer_store::{BufferStore, BufferStoreEvent},
worktree_store::{WorktreeStore, WorktreeStoreEvent},
Project, ProjectItem, ProjectPath,
};
use anyhow::{Context as _, Result};
use buffer_diff::BufferDiffEvent;
use client::ProjectId;
use futures::channel::{mpsc, oneshot};
use futures::StreamExt as _;
use git::repository::{Branch, CommitDetails, PushOptions, Remote, RemoteCommandOutput, ResetMode};
use git::repository::{GitRepository, RepoPath};
use futures::{
channel::{mpsc, oneshot},
StreamExt as _,
};
use git::{
repository::{
Branch, CommitDetails, GitRepository, PushOptions, Remote, RemoteCommandOutput, RepoPath,
ResetMode,
},
status::FileStatus,
};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
WeakEntity,
};
use language::{Buffer, LanguageRegistry};
use rpc::proto::{git_reset, ToProto};
use rpc::{proto, AnyProtoClient, TypedEnvelope};
use rpc::{
proto::{self, git_reset, ToProto},
AnyProtoClient, TypedEnvelope,
};
use settings::WorktreeId;
use std::collections::VecDeque;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{
collections::VecDeque,
future::Future,
path::{Path, PathBuf},
sync::Arc,
};
use text::BufferId;
use util::{maybe, ResultExt};
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
@ -30,7 +44,7 @@ pub struct GitStore {
repositories: Vec<Entity<Repository>>,
active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<GitJob>,
_subscription: Subscription,
_subscriptions: [Subscription; 2],
}
pub struct Repository {
@ -54,10 +68,12 @@ pub enum GitRepo {
},
}
#[derive(Debug)]
pub enum GitEvent {
ActiveRepositoryChanged,
FileSystemUpdated,
GitStateUpdated,
IndexWriteError(anyhow::Error),
}
struct GitJob {
@ -81,7 +97,10 @@ impl GitStore {
cx: &mut Context<'_, Self>,
) -> Self {
let update_sender = Self::spawn_git_worker(cx);
let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
let _subscriptions = [
cx.subscribe(worktree_store, Self::on_worktree_store_event),
cx.subscribe(&buffer_store, Self::on_buffer_store_event),
];
GitStore {
project_id,
@ -90,7 +109,7 @@ impl GitStore {
repositories: Vec::new(),
active_index: None,
update_sender,
_subscription,
_subscriptions,
}
}
@ -227,10 +246,82 @@ impl GitStore {
}
}
fn on_buffer_store_event(
&mut self,
_: Entity<BufferStore>,
event: &BufferStoreEvent,
cx: &mut Context<'_, Self>,
) {
if let BufferStoreEvent::BufferDiffAdded(diff) = event {
cx.subscribe(diff, Self::on_buffer_diff_event).detach();
}
}
fn on_buffer_diff_event(
this: &mut GitStore,
diff: Entity<buffer_diff::BufferDiff>,
event: &BufferDiffEvent,
cx: &mut Context<'_, GitStore>,
) {
if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
let buffer_id = diff.read(cx).buffer_id;
if let Some((repo, path)) = this.repository_and_path_for_buffer_id(buffer_id, cx) {
let recv = repo
.read(cx)
.set_index_text(&path, new_index_text.as_ref().map(|rope| rope.to_string()));
let diff = diff.downgrade();
cx.spawn(|this, mut cx| async move {
if let Some(result) = cx.background_spawn(async move { recv.await.ok() }).await
{
if let Err(error) = result {
diff.update(&mut cx, |diff, cx| {
diff.clear_pending_hunks(cx);
})
.ok();
this.update(&mut cx, |_, cx| cx.emit(GitEvent::IndexWriteError(error)))
.ok();
}
}
})
.detach();
}
}
}
pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
self.repositories.clone()
}
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;
let status = repo.read(cx).repository_entry.status_for_path(&path)?;
Some(status.status)
}
fn repository_and_path_for_buffer_id(
&self,
buffer_id: BufferId,
cx: &App,
) -> Option<(Entity<Repository>, RepoPath)> {
let buffer = self.buffer_store.read(cx).get(buffer_id)?;
let path = buffer.read(cx).project_path(cx)?;
let mut result: Option<(Entity<Repository>, RepoPath)> = None;
for repo_handle in &self.repositories {
let repo = repo_handle.read(cx);
if repo.worktree_id == path.worktree_id {
if let Ok(relative_path) = repo.repository_entry.relativize(&path.path) {
if result
.as_ref()
.is_none_or(|(result, _)| !repo.contains_sub_repo(result, cx))
{
result = Some((repo_handle.clone(), relative_path))
}
}
}
}
result
}
fn spawn_git_worker(cx: &mut Context<'_, GitStore>) -> mpsc::UnboundedSender<GitJob> {
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
@ -658,9 +749,8 @@ impl GitStore {
cx: &mut AsyncApp,
) -> Result<Entity<Repository>> {
this.update(cx, |this, cx| {
let repository_handle = this
.all_repositories()
.into_iter()
this.repositories
.iter()
.find(|repository_handle| {
repository_handle.read(cx).worktree_id == worktree_id
&& repository_handle
@ -669,8 +759,8 @@ impl GitStore {
.work_directory_id()
== work_directory_id
})
.context("missing repository handle")?;
anyhow::Ok(repository_handle)
.context("missing repository handle")
.cloned()
})?
}
}
@ -1297,7 +1387,7 @@ impl Repository {
})
}
pub fn set_index_text(
fn set_index_text(
&self,
path: &RepoPath,
content: Option<String>,