Add an undo button to the git panel (#24593)

Also prep infrastructure for pushing a commit

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
This commit is contained in:
Mikayla Maki 2025-02-12 14:57:08 -08:00 committed by GitHub
parent df8adc8b11
commit b014afa938
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1437 additions and 738 deletions

View file

@ -1,20 +1,22 @@
use crate::buffer_store::BufferStore;
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
use crate::{Project, ProjectPath};
use anyhow::Context as _;
use anyhow::{Context as _, Result};
use client::ProjectId;
use futures::channel::{mpsc, oneshot};
use futures::StreamExt as _;
use git::repository::{Branch, CommitDetails, ResetMode};
use git::{
repository::{GitRepository, RepoPath},
status::{GitSummary, TrackedSummary},
};
use gpui::{
App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, WeakEntity,
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
WeakEntity,
};
use language::{Buffer, LanguageRegistry};
use rpc::proto::ToProto;
use rpc::{proto, AnyProtoClient};
use rpc::proto::{git_reset, ToProto};
use rpc::{proto, AnyProtoClient, TypedEnvelope};
use settings::WorktreeId;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@ -22,22 +24,23 @@ use text::BufferId;
use util::{maybe, ResultExt};
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
pub struct GitState {
pub struct GitStore {
pub(super) project_id: Option<ProjectId>,
pub(super) client: Option<AnyProtoClient>,
pub update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
buffer_store: Entity<BufferStore>,
repositories: Vec<Entity<Repository>>,
active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)>,
_subscription: Subscription,
}
pub struct Repository {
commit_message_buffer: Option<Entity<Buffer>>,
git_state: WeakEntity<GitState>,
git_store: WeakEntity<GitStore>,
pub worktree_id: WorktreeId,
pub repository_entry: RepositoryEntry,
pub git_repo: GitRepo,
update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)>,
}
#[derive(Clone)]
@ -57,6 +60,11 @@ pub enum Message {
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
},
Reset {
repo: GitRepo,
commit: SharedString,
reset_mode: ResetMode,
},
Stage(GitRepo, Vec<RepoPath>),
Unstage(GitRepo, Vec<RepoPath>),
SetIndexText(GitRepo, RepoPath, Option<String>),
@ -68,11 +76,12 @@ pub enum GitEvent {
GitStateUpdated,
}
impl EventEmitter<GitEvent> for GitState {}
impl EventEmitter<GitEvent> for GitStore {}
impl GitState {
impl GitStore {
pub fn new(
worktree_store: &Entity<WorktreeStore>,
buffer_store: Entity<BufferStore>,
client: Option<AnyProtoClient>,
project_id: Option<ProjectId>,
cx: &mut Context<'_, Self>,
@ -80,9 +89,10 @@ impl GitState {
let update_sender = Self::spawn_git_worker(cx);
let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
GitState {
GitStore {
project_id,
client,
buffer_store,
repositories: Vec::new(),
active_index: None,
update_sender,
@ -90,6 +100,16 @@ impl GitState {
}
}
pub fn init(client: &AnyProtoClient) {
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_reset);
client.add_entity_request_handler(Self::handle_show);
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
client.add_entity_request_handler(Self::handle_set_index_text);
}
pub fn active_repository(&self) -> Option<Entity<Repository>> {
self.active_index
.map(|index| self.repositories[index].clone())
@ -153,7 +173,7 @@ impl GitState {
existing_handle
} else {
cx.new(|_| Repository {
git_state: this.clone(),
git_store: this.clone(),
worktree_id,
repository_entry: repo.clone(),
git_repo,
@ -189,10 +209,10 @@ impl GitState {
}
fn spawn_git_worker(
cx: &mut Context<'_, GitState>,
) -> mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)> {
cx: &mut Context<'_, GitStore>,
) -> mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)> {
let (update_sender, mut update_receiver) =
mpsc::unbounded::<(Message, oneshot::Sender<anyhow::Result<()>>)>();
mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
cx.spawn(|_, cx| async move {
while let Some((msg, respond)) = update_receiver.next().await {
let result = cx
@ -206,7 +226,7 @@ impl GitState {
update_sender
}
async fn process_git_msg(msg: Message) -> Result<(), anyhow::Error> {
async fn process_git_msg(msg: Message) -> Result<()> {
match msg {
Message::Stage(repo, paths) => {
match repo {
@ -233,6 +253,35 @@ impl GitState {
}
Ok(())
}
Message::Reset {
repo,
commit,
reset_mode,
} => {
match repo {
GitRepo::Local(repo) => repo.reset(&commit, reset_mode)?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::GitReset {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
commit: commit.into(),
mode: match reset_mode {
ResetMode::Soft => git_reset::ResetMode::Soft.into(),
ResetMode::Mixed => git_reset::ResetMode::Mixed.into(),
},
})
.await?;
}
}
Ok(())
}
Message::Unstage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
@ -309,20 +358,219 @@ impl GitState {
},
}
}
async fn handle_stage(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Stage>,
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)?;
let entries = envelope
.payload
.paths
.into_iter()
.map(PathBuf::from)
.map(RepoPath::new)
.collect();
repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.stage_entries(entries)
})?
.await??;
Ok(proto::Ack {})
}
async fn handle_unstage(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Unstage>,
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)?;
let entries = envelope
.payload
.paths
.into_iter()
.map(PathBuf::from)
.map(RepoPath::new)
.collect();
repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.unstage_entries(entries)
})?
.await??;
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_commit(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Commit>,
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)?;
let message = SharedString::from(envelope.payload.message);
let name = envelope.payload.name.map(SharedString::from);
let email = envelope.payload.email.map(SharedString::from);
repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.commit(message, name.zip(email))
})?
.await??;
Ok(proto::Ack {})
}
async fn handle_show(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitShow>,
mut cx: AsyncApp,
) -> Result<proto::GitCommitDetails> {
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)?;
let commit = repository_handle
.update(&mut cx, |repository_handle, cx| {
repository_handle.show(&envelope.payload.commit, cx)
})?
.await?;
Ok(proto::GitCommitDetails {
sha: commit.sha.into(),
message: commit.message.into(),
commit_timestamp: commit.commit_timestamp,
committer_email: commit.committer_email.into(),
committer_name: commit.committer_name.into(),
})
}
async fn handle_reset(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitReset>,
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)?;
let mode = match envelope.payload.mode() {
git_reset::ResetMode::Soft => ResetMode::Soft,
git_reset::ResetMode::Mixed => ResetMode::Mixed,
};
repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.reset(&envelope.payload.commit, mode)
})?
.await??;
Ok(proto::Ack {})
}
async fn handle_open_commit_message_buffer(
this: Entity<Self>,
envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
mut cx: AsyncApp,
) -> Result<proto::OpenBufferResponse> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let buffer = repository
.update(&mut cx, |repository, cx| {
repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
})?
.await?;
let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?;
this.update(&mut cx, |this, cx| {
this.buffer_store.update(cx, |buffer_store, cx| {
buffer_store
.create_buffer_for_peer(
&buffer,
envelope.original_sender_id.unwrap_or(envelope.sender_id),
cx,
)
.detach_and_log_err(cx);
})
})?;
Ok(proto::OpenBufferResponse {
buffer_id: buffer_id.to_proto(),
})
}
fn repository_for_request(
this: &Entity<Self>,
worktree_id: WorktreeId,
work_directory_id: ProjectEntryId,
cx: &mut AsyncApp,
) -> Result<Entity<Repository>> {
this.update(cx, |this, cx| {
let repository_handle = this
.all_repositories()
.into_iter()
.find(|repository_handle| {
repository_handle.read(cx).worktree_id == worktree_id
&& repository_handle
.read(cx)
.repository_entry
.work_directory_id()
== work_directory_id
})
.context("missing repository handle")?;
anyhow::Ok(repository_handle)
})?
}
}
impl GitRepo {}
impl Repository {
pub fn git_state(&self) -> Option<Entity<GitState>> {
self.git_state.upgrade()
pub fn git_store(&self) -> Option<Entity<GitStore>> {
self.git_store.upgrade()
}
fn id(&self) -> (WorktreeId, ProjectEntryId) {
(self.worktree_id, self.repository_entry.work_directory_id())
}
pub fn branch(&self) -> Option<Arc<str>> {
pub fn branch(&self) -> Option<&Branch> {
self.repository_entry.branch()
}
@ -344,19 +592,19 @@ impl Repository {
}
pub fn activate(&self, cx: &mut Context<Self>) {
let Some(git_state) = self.git_state.upgrade() else {
let Some(git_store) = self.git_store.upgrade() else {
return;
};
let entity = cx.entity();
git_state.update(cx, |git_state, cx| {
let Some(index) = git_state
git_store.update(cx, |git_store, cx| {
let Some(index) = git_store
.repositories
.iter()
.position(|handle| *handle == entity)
else {
return;
};
git_state.active_index = Some(index);
git_store.active_index = Some(index);
cx.emit(GitEvent::ActiveRepositoryChanged);
});
}
@ -396,7 +644,7 @@ impl Repository {
languages: Option<Arc<LanguageRegistry>>,
buffer_store: Entity<BufferStore>,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<Entity<Buffer>>> {
) -> Task<Result<Entity<Buffer>>> {
if let Some(buffer) = self.commit_message_buffer.clone() {
return Task::ready(Ok(buffer));
}
@ -444,7 +692,7 @@ impl Repository {
language_registry: Option<Arc<LanguageRegistry>>,
buffer_store: Entity<BufferStore>,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<Entity<Buffer>>> {
) -> Task<Result<Entity<Buffer>>> {
cx.spawn(|repository, mut cx| async move {
let buffer = buffer_store
.update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
@ -464,7 +712,57 @@ impl Repository {
})
}
pub fn stage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
let commit = commit.to_string().into();
self.update_sender
.unbounded_send((
Message::Reset {
repo: self.git_repo.clone(),
commit,
reset_mode,
},
result_tx,
))
.ok();
result_rx
}
pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
let commit = commit.to_string();
match self.git_repo.clone() {
GitRepo::Local(git_repository) => {
let commit = commit.to_string();
cx.background_executor()
.spawn(async move { git_repository.show(&commit) })
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => cx.background_executor().spawn(async move {
let resp = client
.request(proto::GitShow {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
commit,
})
.await?;
Ok(CommitDetails {
sha: resp.sha.into(),
message: resp.message.into(),
commit_timestamp: resp.commit_timestamp,
committer_email: resp.committer_email.into(),
committer_name: resp.committer_name.into(),
})
}),
}
}
pub fn stage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
if entries.is_empty() {
result_tx.send(Ok(())).ok();
@ -476,7 +774,7 @@ impl Repository {
result_rx
}
pub fn unstage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
pub fn unstage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
if entries.is_empty() {
result_tx.send(Ok(())).ok();
@ -488,7 +786,7 @@ impl Repository {
result_rx
}
pub fn stage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
pub fn stage_all(&self) -> oneshot::Receiver<Result<()>> {
let to_stage = self
.repository_entry
.status()
@ -498,7 +796,7 @@ impl Repository {
self.stage_entries(to_stage)
}
pub fn unstage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
pub fn unstage_all(&self) -> oneshot::Receiver<Result<()>> {
let to_unstage = self
.repository_entry
.status()
@ -530,7 +828,7 @@ impl Repository {
&self,
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
) -> oneshot::Receiver<anyhow::Result<()>> {
) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender
.unbounded_send((