Git Panel: separate new and changed (#24181)

Release Notes:

- N/A

---------

Co-authored-by: conrad <conrad@zed.dev>
Co-authored-by: nate <nate@zed.dev>
This commit is contained in:
Mikayla Maki 2025-02-04 01:15:09 -08:00 committed by GitHub
parent 6659aea13b
commit 71f2cbe798
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 666 additions and 589 deletions

View file

@ -153,6 +153,7 @@ impl FileStatus {
(StatusCode::Added, _) | (_, StatusCode::Added) => true, (StatusCode::Added, _) | (_, StatusCode::Added) => true,
_ => false, _ => false,
}, },
FileStatus::Untracked => true,
_ => false, _ => false,
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,8 @@ use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
use crate::{Project, ProjectPath}; use crate::{Project, ProjectPath};
use anyhow::{anyhow, Context as _}; use anyhow::{anyhow, Context as _};
use client::ProjectId; use client::ProjectId;
use futures::channel::mpsc; use futures::channel::{mpsc, oneshot};
use futures::{SinkExt as _, StreamExt as _}; use futures::StreamExt as _;
use git::{ use git::{
repository::{GitRepository, RepoPath}, repository::{GitRepository, RepoPath},
status::{GitSummary, TrackedSummary}, status::{GitSummary, TrackedSummary},
@ -20,7 +20,7 @@ pub struct GitState {
client: Option<AnyProtoClient>, client: Option<AnyProtoClient>,
repositories: Vec<RepositoryHandle>, repositories: Vec<RepositoryHandle>,
active_index: Option<usize>, active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>, update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
_subscription: Subscription, _subscription: Subscription,
} }
@ -30,7 +30,7 @@ pub struct RepositoryHandle {
pub worktree_id: WorktreeId, pub worktree_id: WorktreeId,
pub repository_entry: RepositoryEntry, pub repository_entry: RepositoryEntry,
pub git_repo: GitRepo, pub git_repo: GitRepo,
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>, update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -61,11 +61,6 @@ impl PartialEq<RepositoryEntry> for RepositoryHandle {
} }
enum Message { enum Message {
StageAndCommit {
git_repo: GitRepo,
paths: Vec<RepoPath>,
name_and_email: Option<(SharedString, SharedString)>,
},
Commit { Commit {
git_repo: GitRepo, git_repo: GitRepo,
name_and_email: Option<(SharedString, SharedString)>, name_and_email: Option<(SharedString, SharedString)>,
@ -87,151 +82,7 @@ impl GitState {
project_id: Option<ProjectId>, project_id: Option<ProjectId>,
cx: &mut Context<'_, Self>, cx: &mut Context<'_, Self>,
) -> Self { ) -> Self {
let (update_sender, mut update_receiver) = let update_sender = Self::spawn_git_worker(cx);
mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
cx.spawn(|_, cx| async move {
while let Some((msg, mut err_sender)) = update_receiver.next().await {
let result =
cx.background_executor()
.spawn(async move {
match msg {
Message::StageAndCommit {
git_repo,
name_and_email,
paths,
} => {
match git_repo {
GitRepo::Local(repo) => {
repo.stage_paths(&paths)?;
repo.commit(name_and_email.as_ref().map(
|(name, email)| (name.as_ref(), email.as_ref()),
))?;
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Stage {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
paths: paths
.into_iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
.context("sending stage request")?;
let (name, email) = name_and_email.unzip();
client
.request(proto::Commit {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
name: name.map(String::from),
email: email.map(String::from),
})
.await
.context("sending commit request")?;
}
}
Ok(())
}
Message::Stage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.stage_paths(&paths)?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Stage {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
paths: paths
.into_iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
.context("sending stage request")?;
}
}
Ok(())
}
Message::Unstage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Unstage {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
paths: paths
.into_iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
.context("sending unstage request")?;
}
}
Ok(())
}
Message::Commit {
git_repo,
name_and_email,
} => {
match git_repo {
GitRepo::Local(repo) => {
repo.commit(name_and_email.as_ref().map(
|(name, email)| (name.as_ref(), email.as_ref()),
))?
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
let (name, email) = name_and_email.unzip();
client
.request(proto::Commit {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
name: name.map(String::from),
email: email.map(String::from),
})
.await
.context("sending commit request")?;
}
}
Ok(())
}
}
})
.await;
if let Err(e) = result {
err_sender.send(e).await.ok();
}
}
})
.detach();
let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event); let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
GitState { GitState {
@ -327,6 +178,110 @@ impl GitState {
pub fn all_repositories(&self) -> Vec<RepositoryHandle> { pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
self.repositories.clone() self.repositories.clone()
} }
fn spawn_git_worker(
cx: &mut Context<'_, GitState>,
) -> mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)> {
let (update_sender, mut update_receiver) =
mpsc::unbounded::<(Message, oneshot::Sender<anyhow::Result<()>>)>();
cx.spawn(|_, cx| async move {
while let Some((msg, respond)) = update_receiver.next().await {
let result = cx
.background_executor()
.spawn(Self::process_git_msg(msg))
.await;
respond.send(result).ok();
}
})
.detach();
update_sender
}
async fn process_git_msg(msg: Message) -> Result<(), anyhow::Error> {
match msg {
Message::Stage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.stage_paths(&paths)?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Stage {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
paths: paths
.into_iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
.context("sending stage request")?;
}
}
Ok(())
}
Message::Unstage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Unstage {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
paths: paths
.into_iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
})
.await
.context("sending unstage request")?;
}
}
Ok(())
}
Message::Commit {
git_repo,
name_and_email,
} => {
match git_repo {
GitRepo::Local(repo) => repo.commit(
name_and_email
.as_ref()
.map(|(name, email)| (name.as_ref(), email.as_ref())),
)?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
let (name, email) = name_and_email.unzip();
client
.request(proto::Commit {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
name: name.map(String::from),
email: email.map(String::from),
})
.await
.context("sending commit request")?;
}
}
Ok(())
}
}
}
} }
impl RepositoryHandle { impl RepositoryHandle {
@ -379,54 +334,47 @@ impl RepositoryHandle {
self.repository_entry.relativize(&path.path).log_err() self.repository_entry.relativize(&path.path).log_err()
} }
pub fn stage_entries( pub async fn stage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
&self,
entries: Vec<RepoPath>,
err_sender: mpsc::Sender<anyhow::Error>,
) -> anyhow::Result<()> {
if entries.is_empty() { if entries.is_empty() {
return Ok(()); return Ok(());
} }
let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender self.update_sender
.unbounded_send((Message::Stage(self.git_repo.clone(), entries), err_sender)) .unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
.map_err(|_| anyhow!("Failed to submit stage operation"))?; .map_err(|_| anyhow!("Failed to submit stage operation"))?;
Ok(())
result_rx.await?
} }
pub fn unstage_entries( pub async fn unstage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
&self,
entries: Vec<RepoPath>,
err_sender: mpsc::Sender<anyhow::Error>,
) -> anyhow::Result<()> {
if entries.is_empty() { if entries.is_empty() {
return Ok(()); return Ok(());
} }
let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender self.update_sender
.unbounded_send((Message::Unstage(self.git_repo.clone(), entries), err_sender)) .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
.map_err(|_| anyhow!("Failed to submit unstage operation"))?; .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
Ok(()) result_rx.await?
} }
pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> { pub async fn stage_all(&self) -> anyhow::Result<()> {
let to_stage = self let to_stage = self
.repository_entry .repository_entry
.status() .status()
.filter(|entry| !entry.status.is_staged().unwrap_or(false)) .filter(|entry| !entry.status.is_staged().unwrap_or(false))
.map(|entry| entry.repo_path.clone()) .map(|entry| entry.repo_path.clone())
.collect(); .collect();
self.stage_entries(to_stage, err_sender)?; self.stage_entries(to_stage).await
Ok(())
} }
pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> { pub async fn unstage_all(&self) -> anyhow::Result<()> {
let to_unstage = self let to_unstage = self
.repository_entry .repository_entry
.status() .status()
.filter(|entry| entry.status.is_staged().unwrap_or(true)) .filter(|entry| entry.status.is_staged().unwrap_or(true))
.map(|entry| entry.repo_path.clone()) .map(|entry| entry.repo_path.clone())
.collect(); .collect();
self.unstage_entries(to_unstage, err_sender)?; self.unstage_entries(to_unstage).await
Ok(())
} }
/// Get a count of all entries in the active repository, including /// Get a count of all entries in the active repository, including
@ -447,64 +395,18 @@ impl RepositoryHandle {
return self.have_changes() && (commit_all || self.have_staged_changes()); return self.have_changes() && (commit_all || self.have_staged_changes());
} }
pub fn commit( pub async fn commit(
&self, &self,
name_and_email: Option<(SharedString, SharedString)>, name_and_email: Option<(SharedString, SharedString)>,
mut err_sender: mpsc::Sender<anyhow::Error>,
cx: &mut App,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let result = self.update_sender.unbounded_send(( let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender.unbounded_send((
Message::Commit { Message::Commit {
git_repo: self.git_repo.clone(), git_repo: self.git_repo.clone(),
name_and_email, name_and_email,
}, },
err_sender.clone(), result_tx,
)); ))?;
if result.is_err() { result_rx.await?
cx.spawn(|_| async move {
err_sender
.send(anyhow!("Failed to submit commit operation"))
.await
.ok();
})
.detach();
anyhow::bail!("Failed to submit commit operation");
} else {
Ok(())
}
}
pub fn commit_all(
&self,
name_and_email: Option<(SharedString, SharedString)>,
mut err_sender: mpsc::Sender<anyhow::Error>,
cx: &mut App,
) -> anyhow::Result<()> {
let to_stage = self
.repository_entry
.status()
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
.map(|entry| entry.repo_path.clone())
.collect();
let result = self.update_sender.unbounded_send((
Message::StageAndCommit {
git_repo: self.git_repo.clone(),
paths: to_stage,
name_and_email,
},
err_sender.clone(),
));
if result.is_err() {
cx.spawn(|_| async move {
err_sender
.send(anyhow!("Failed to submit commit all operation"))
.await
.ok();
})
.detach();
anyhow::bail!("Failed to submit commit all operation");
} else {
Ok(())
}
} }
} }

View file

@ -4006,15 +4006,9 @@ impl Project {
.map(PathBuf::from) .map(PathBuf::from)
.map(RepoPath::new) .map(RepoPath::new)
.collect(); .collect();
let (err_sender, mut err_receiver) = mpsc::channel(1);
repository_handle repository_handle.stage_entries(entries).await?;
.stage_entries(entries, err_sender) Ok(proto::Ack {})
.context("staging entries")?;
if let Some(error) = err_receiver.next().await {
Err(error.context("error during staging"))
} else {
Ok(proto::Ack {})
}
} }
async fn handle_unstage( async fn handle_unstage(
@ -4034,15 +4028,9 @@ impl Project {
.map(PathBuf::from) .map(PathBuf::from)
.map(RepoPath::new) .map(RepoPath::new)
.collect(); .collect();
let (err_sender, mut err_receiver) = mpsc::channel(1);
repository_handle repository_handle.unstage_entries(entries).await?;
.unstage_entries(entries, err_sender) Ok(proto::Ack {})
.context("unstaging entries")?;
if let Some(error) = err_receiver.next().await {
Err(error.context("error during unstaging"))
} else {
Ok(proto::Ack {})
}
} }
async fn handle_commit( async fn handle_commit(
@ -4057,17 +4045,8 @@ impl Project {
let name = envelope.payload.name.map(SharedString::from); let name = envelope.payload.name.map(SharedString::from);
let email = envelope.payload.email.map(SharedString::from); let email = envelope.payload.email.map(SharedString::from);
let (err_sender, mut err_receiver) = mpsc::channel(1); repository_handle.commit(name.zip(email)).await?;
cx.update(|cx| { Ok(proto::Ack {})
repository_handle
.commit(name.zip(email), err_sender, cx)
.context("unstaging entries")
})??;
if let Some(error) = err_receiver.next().await {
Err(error.context("error during unstaging"))
} else {
Ok(proto::Ack {})
}
} }
async fn handle_open_commit_message_buffer( async fn handle_open_commit_message_buffer(

View file

@ -2,7 +2,6 @@ use anyhow::{anyhow, Context as _, Result};
use extension::ExtensionHostProxy; use extension::ExtensionHostProxy;
use extension_host::headless_host::HeadlessExtensionStore; use extension_host::headless_host::HeadlessExtensionStore;
use fs::{CreateOptions, Fs}; use fs::{CreateOptions, Fs};
use futures::channel::mpsc;
use git::{repository::RepoPath, COMMIT_MESSAGE}; use git::{repository::RepoPath, COMMIT_MESSAGE};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, SharedString}; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, SharedString};
use http_client::HttpClient; use http_client::HttpClient;
@ -637,15 +636,9 @@ impl HeadlessProject {
.map(PathBuf::from) .map(PathBuf::from)
.map(RepoPath::new) .map(RepoPath::new)
.collect(); .collect();
let (err_sender, mut err_receiver) = mpsc::channel(1);
repository_handle repository_handle.stage_entries(entries).await?;
.stage_entries(entries, err_sender) Ok(proto::Ack {})
.context("staging entries")?;
if let Some(error) = err_receiver.next().await {
Err(error.context("error during staging"))
} else {
Ok(proto::Ack {})
}
} }
async fn handle_unstage( async fn handle_unstage(
@ -665,15 +658,10 @@ impl HeadlessProject {
.map(PathBuf::from) .map(PathBuf::from)
.map(RepoPath::new) .map(RepoPath::new)
.collect(); .collect();
let (err_sender, mut err_receiver) = mpsc::channel(1);
repository_handle repository_handle.unstage_entries(entries).await?;
.unstage_entries(entries, err_sender)
.context("unstaging entries")?; Ok(proto::Ack {})
if let Some(error) = err_receiver.next().await {
Err(error.context("error during unstaging"))
} else {
Ok(proto::Ack {})
}
} }
async fn handle_commit( async fn handle_commit(
@ -688,17 +676,9 @@ impl HeadlessProject {
let name = envelope.payload.name.map(SharedString::from); let name = envelope.payload.name.map(SharedString::from);
let email = envelope.payload.email.map(SharedString::from); let email = envelope.payload.email.map(SharedString::from);
let (err_sender, mut err_receiver) = mpsc::channel(1);
cx.update(|cx| { repository_handle.commit(name.zip(email)).await?;
repository_handle Ok(proto::Ack {})
.commit(name.zip(email), err_sender, cx)
.context("unstaging entries")
})??;
if let Some(error) = err_receiver.next().await {
Err(error.context("error during unstaging"))
} else {
Ok(proto::Ack {})
}
} }
async fn handle_open_commit_message_buffer( async fn handle_open_commit_message_buffer(

View file

@ -135,6 +135,11 @@ impl Checkbox {
ToggleStyle::Custom(color) => color.opacity(0.3), ToggleStyle::Custom(color) => color.opacity(0.3),
} }
} }
/// container size
pub fn container_size(cx: &App) -> Rems {
DynamicSpacing::Base20.rems(cx)
}
} }
impl RenderOnce for Checkbox { impl RenderOnce for Checkbox {
@ -163,9 +168,13 @@ impl RenderOnce for Checkbox {
let bg_color = self.bg_color(cx); let bg_color = self.bg_color(cx);
let border_color = self.border_color(cx); let border_color = self.border_color(cx);
let size = Self::container_size(cx);
let checkbox = h_flex() let checkbox = h_flex()
.id(self.id.clone())
.justify_center() .justify_center()
.size(DynamicSpacing::Base20.rems(cx)) .items_center()
.size(size)
.group(group_id.clone()) .group(group_id.clone())
.child( .child(
div() div()

View file

@ -29,6 +29,23 @@ impl ToggleState {
Self::Selected => Self::Unselected, Self::Selected => Self::Unselected,
} }
} }
/// Creates a `ToggleState` from the given `any_checked` and `all_checked` flags.
pub fn from_any_and_all(any_checked: bool, all_checked: bool) -> Self {
match (any_checked, all_checked) {
(true, true) => Self::Selected,
(false, false) => Self::Unselected,
_ => Self::Indeterminate,
}
}
/// Returns whether this toggle state is selected
pub fn selected(&self) -> bool {
match self {
ToggleState::Indeterminate | ToggleState::Unselected => false,
ToggleState::Selected => true,
}
}
} }
impl From<bool> for ToggleState { impl From<bool> for ToggleState {