Implement staging and unstaging hunks (#24606)

- [x] Staging hunks
- [x] Unstaging hunks
- [x] Write a randomized test
- [x] Get test passing
- [x] Fix existing bug in diff_base_byte_range computation
- [x] Remote project support
- [ ] ~~Improve performance of
buffer_range_to_unchanged_diff_base_range~~
- [ ] ~~Bug: project diff editor scrolls to top when staging/unstaging
hunk~~ existing issue
- [ ] ~~UI~~ deferred
- [x] Tricky cases
  - [x] Correctly handle acting on multiple hunks for a single file
- [x] Remove path from index when unstaging the last staged hunk, if
it's absent from HEAD, or staging the only hunk, if it's deleted in the
working copy

Release Notes:

- Add `ToggleStagedSelectedDiffHunks` action for staging and unstaging
individual diff hunks
This commit is contained in:
Cole Miller 2025-02-12 14:46:42 -05:00 committed by GitHub
parent ea8da43c6b
commit eea6b526dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 768 additions and 70 deletions

View file

@ -189,6 +189,7 @@ impl BufferDiffState {
buffer: text::BufferSnapshot,
cx: &mut Context<Self>,
) -> oneshot::Receiver<()> {
log::debug!("recalculate diffs");
let (tx, rx) = oneshot::channel();
self.diff_updated_futures.push(tx);

View file

@ -23,11 +23,11 @@ use util::{maybe, ResultExt};
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
pub struct GitState {
project_id: Option<ProjectId>,
client: Option<AnyProtoClient>,
pub(super) project_id: Option<ProjectId>,
pub(super) client: Option<AnyProtoClient>,
pub update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
repositories: Vec<Entity<Repository>>,
active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
_subscription: Subscription,
}
@ -51,7 +51,7 @@ pub enum GitRepo {
},
}
enum Message {
pub enum Message {
Commit {
git_repo: GitRepo,
message: SharedString,
@ -59,6 +59,7 @@ enum Message {
},
Stage(GitRepo, Vec<RepoPath>),
Unstage(GitRepo, Vec<RepoPath>),
SetIndexText(GitRepo, RepoPath, Option<String>),
}
pub enum GitEvent {
@ -291,11 +292,32 @@ impl GitState {
}
Ok(())
}
Message::SetIndexText(git_repo, path, text) => match git_repo {
GitRepo::Local(repo) => repo.set_index_text(&path, text),
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => client.send(proto::SetIndexText {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
path: path.as_ref().to_proto(),
text,
}),
},
}
}
}
impl GitRepo {}
impl Repository {
pub fn git_state(&self) -> Option<Entity<GitState>> {
self.git_state.upgrade()
}
fn id(&self) -> (WorktreeId, ProjectEntryId) {
(self.worktree_id, self.repository_entry.work_directory_id())
}
@ -522,4 +544,19 @@ impl Repository {
.ok();
result_rx
}
pub fn set_index_text(
&self,
path: &RepoPath,
content: Option<String>,
) -> oneshot::Receiver<anyhow::Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender
.unbounded_send((
Message::SetIndexText(self.git_repo.clone(), path.clone(), content),
result_tx,
))
.ok();
result_rx
}
}

View file

@ -610,6 +610,7 @@ impl Project {
client.add_entity_request_handler(Self::handle_stage);
client.add_entity_request_handler(Self::handle_unstage);
client.add_entity_request_handler(Self::handle_commit);
client.add_entity_request_handler(Self::handle_set_index_text);
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
WorktreeStore::init(&client);
@ -4092,6 +4093,27 @@ impl Project {
Ok(proto::Ack {})
}
async fn handle_set_index_text(
this: Entity<Self>,
envelope: TypedEnvelope<proto::SetIndexText>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.set_index_text(
&RepoPath::from_str(&envelope.payload.path),
envelope.payload.text,
)
})?
.await??;
Ok(proto::Ack {})
}
async fn handle_open_commit_message_buffer(
this: Entity<Self>,
envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
@ -4336,6 +4358,27 @@ impl Project {
pub fn all_repositories(&self, cx: &App) -> Vec<Entity<Repository>> {
self.git_state.read(cx).all_repositories()
}
pub fn repository_and_path_for_buffer_id(
&self,
buffer_id: BufferId,
cx: &App,
) -> Option<(Entity<Repository>, RepoPath)> {
let path = self
.buffer_for_id(buffer_id, cx)?
.read(cx)
.project_path(cx)?;
self.git_state
.read(cx)
.all_repositories()
.into_iter()
.find_map(|repo| {
Some((
repo.clone(),
repo.read(cx).repository_entry.relativize(&path.path).ok()?,
))
})
}
}
fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::CodeActionKind> {