Cargo.lock 🔗
@@ -5192,6 +5192,7 @@ dependencies = [
"collections",
"db",
"editor",
+ "futures 0.3.31",
"git",
"gpui",
"language",
Cole Miller and Nate created
- [x] Basic implementation
- [x] Disable commit buttons when committing is not possible (empty
message, no changes)
- [x] Upgrade GitSummary to efficiently figure out whether there are any
staged changes
- [x] Make CommitAll work
- [x] Surface errors with toasts
- [x] Channel shutdown
- [x] Empty commit message or no changes
- [x] Failed git operations
- [x] Fix added files no longer appearing correctly in the project panel
(GitSummary breakage)
- [x] Fix handling of commit message
Release Notes:
- N/A
---------
Co-authored-by: Nate <nate@zed.dev>
Cargo.lock | 1
crates/editor/src/items.rs | 5
crates/git/src/repository.rs | 24 +++
crates/git/src/status.rs | 130 ++++++++++++++----
crates/git_ui/Cargo.toml | 1
crates/git_ui/src/git_panel.rs | 144 ++++++++++++-------
crates/project/src/git.rs | 177 ++++++++++++++++++------
crates/project_panel/src/project_panel.rs | 4
crates/worktree/src/worktree.rs | 4
crates/worktree/src/worktree_tests.rs | 93 ++++--------
10 files changed, 386 insertions(+), 197 deletions(-)
@@ -5192,6 +5192,7 @@ dependencies = [
"collections",
"db",
"editor",
+ "futures 0.3.31",
"git",
"gpui",
"language",
@@ -1560,13 +1560,14 @@ pub fn entry_diagnostic_aware_icon_decoration_and_color(
}
pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
+ let tracked = git_status.index + git_status.worktree;
if ignored {
Color::Ignored
} else if git_status.conflict > 0 {
Color::Conflict
- } else if git_status.modified > 0 {
+ } else if tracked.modified > 0 {
Color::Modified
- } else if git_status.added > 0 || git_status.untracked > 0 {
+ } else if tracked.added > 0 || git_status.untracked > 0 {
Color::Created
} else {
entry_label_color(selected)
@@ -61,6 +61,8 @@ pub trait GitRepository: Send + Sync {
///
/// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
+
+ fn commit(&self, message: &str) -> Result<()>;
}
impl std::fmt::Debug for dyn GitRepository {
@@ -280,6 +282,24 @@ impl GitRepository for RealGitRepository {
}
Ok(())
}
+
+ fn commit(&self, message: &str) -> Result<()> {
+ let working_directory = self
+ .repository
+ .lock()
+ .workdir()
+ .context("failed to read git work directory")?
+ .to_path_buf();
+
+ let cmd = new_std_command(&self.git_binary_path)
+ .current_dir(&working_directory)
+ .args(["commit", "--quiet", "-m", message])
+ .status()?;
+ if !cmd.success() {
+ return Err(anyhow!("Failed to commit: {cmd}"));
+ }
+ Ok(())
+ }
}
#[derive(Debug, Clone)]
@@ -423,6 +443,10 @@ impl GitRepository for FakeGitRepository {
fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
unimplemented!()
}
+
+ fn commit(&self, _message: &str) -> Result<()> {
+ unimplemented!()
+ }
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@@ -171,13 +171,13 @@ impl FileStatus {
FileStatus::Tracked(TrackedStatus {
index_status,
worktree_status,
- }) => {
- let mut summary = index_status.to_summary() + worktree_status.to_summary();
- if summary != GitSummary::UNCHANGED {
- summary.count = 1;
- };
- summary
- }
+ }) => GitSummary {
+ index: index_status.to_summary(),
+ worktree: worktree_status.to_summary(),
+ conflict: 0,
+ untracked: 0,
+ count: 1,
+ },
}
}
}
@@ -196,28 +196,39 @@ impl StatusCode {
}
}
- /// Returns the contribution of this status code to the Git summary.
- ///
- /// Note that this does not include the count field, which must be set manually.
- fn to_summary(self) -> GitSummary {
+ fn to_summary(self) -> TrackedSummary {
match self {
- StatusCode::Modified | StatusCode::TypeChanged => GitSummary {
+ StatusCode::Modified | StatusCode::TypeChanged => TrackedSummary {
modified: 1,
- ..GitSummary::UNCHANGED
+ ..TrackedSummary::UNCHANGED
},
- StatusCode::Added => GitSummary {
+ StatusCode::Added => TrackedSummary {
added: 1,
- ..GitSummary::UNCHANGED
+ ..TrackedSummary::UNCHANGED
},
- StatusCode::Deleted => GitSummary {
+ StatusCode::Deleted => TrackedSummary {
deleted: 1,
- ..GitSummary::UNCHANGED
+ ..TrackedSummary::UNCHANGED
},
StatusCode::Renamed | StatusCode::Copied | StatusCode::Unmodified => {
- GitSummary::UNCHANGED
+ TrackedSummary::UNCHANGED
}
}
}
+
+ pub fn index(self) -> FileStatus {
+ FileStatus::Tracked(TrackedStatus {
+ index_status: self,
+ worktree_status: StatusCode::Unmodified,
+ })
+ }
+
+ pub fn worktree(self) -> FileStatus {
+ FileStatus::Tracked(TrackedStatus {
+ index_status: StatusCode::Unmodified,
+ worktree_status: self,
+ })
+ }
}
impl UnmergedStatusCode {
@@ -232,12 +243,76 @@ impl UnmergedStatusCode {
}
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
-pub struct GitSummary {
+pub struct TrackedSummary {
pub added: usize,
pub modified: usize,
+ pub deleted: usize,
+}
+
+impl TrackedSummary {
+ pub const UNCHANGED: Self = Self {
+ added: 0,
+ modified: 0,
+ deleted: 0,
+ };
+
+ pub const ADDED: Self = Self {
+ added: 1,
+ modified: 0,
+ deleted: 0,
+ };
+
+ pub const MODIFIED: Self = Self {
+ added: 0,
+ modified: 1,
+ deleted: 0,
+ };
+
+ pub const DELETED: Self = Self {
+ added: 0,
+ modified: 0,
+ deleted: 1,
+ };
+}
+
+impl std::ops::AddAssign for TrackedSummary {
+ fn add_assign(&mut self, rhs: Self) {
+ self.added += rhs.added;
+ self.modified += rhs.modified;
+ self.deleted += rhs.deleted;
+ }
+}
+
+impl std::ops::Add for TrackedSummary {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ TrackedSummary {
+ added: self.added + rhs.added,
+ modified: self.modified + rhs.modified,
+ deleted: self.deleted + rhs.deleted,
+ }
+ }
+}
+
+impl std::ops::Sub for TrackedSummary {
+ type Output = Self;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ TrackedSummary {
+ added: self.added - rhs.added,
+ modified: self.modified - rhs.modified,
+ deleted: self.deleted - rhs.deleted,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
+pub struct GitSummary {
+ pub index: TrackedSummary,
+ pub worktree: TrackedSummary,
pub conflict: usize,
pub untracked: usize,
- pub deleted: usize,
pub count: usize,
}
@@ -255,11 +330,10 @@ impl GitSummary {
};
pub const UNCHANGED: Self = Self {
- added: 0,
- modified: 0,
+ index: TrackedSummary::UNCHANGED,
+ worktree: TrackedSummary::UNCHANGED,
conflict: 0,
untracked: 0,
- deleted: 0,
count: 0,
};
}
@@ -293,11 +367,10 @@ impl std::ops::Add<Self> for GitSummary {
impl std::ops::AddAssign for GitSummary {
fn add_assign(&mut self, rhs: Self) {
- self.added += rhs.added;
- self.modified += rhs.modified;
+ self.index += rhs.index;
+ self.worktree += rhs.worktree;
self.conflict += rhs.conflict;
self.untracked += rhs.untracked;
- self.deleted += rhs.deleted;
self.count += rhs.count;
}
}
@@ -307,11 +380,10 @@ impl std::ops::Sub for GitSummary {
fn sub(self, rhs: Self) -> Self::Output {
GitSummary {
- added: self.added - rhs.added,
- modified: self.modified - rhs.modified,
+ index: self.index - rhs.index,
+ worktree: self.worktree - rhs.worktree,
conflict: self.conflict - rhs.conflict,
untracked: self.untracked - rhs.untracked,
- deleted: self.deleted - rhs.deleted,
count: self.count - rhs.count,
}
}
@@ -17,6 +17,7 @@ anyhow.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
+futures.workspace = true
git.workspace = true
gpui.workspace = true
language.workspace = true
@@ -4,6 +4,8 @@ use anyhow::{Context as _, Result};
use db::kvp::KEY_VALUE_STORE;
use editor::scroll::ScrollbarAutoHide;
use editor::{Editor, EditorSettings, ShowScrollbar};
+use futures::channel::mpsc;
+use futures::StreamExt as _;
use git::repository::{GitRepository, RepoPath};
use git::status::FileStatus;
use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll};
@@ -21,7 +23,8 @@ use ui::{
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
};
use util::{maybe, ResultExt, TryFutureExt};
-use workspace::notifications::DetachAndPromptErr;
+use workspace::notifications::{DetachAndPromptErr, NotificationId};
+use workspace::Toast;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Workspace,
@@ -76,6 +79,7 @@ pub struct GitListEntry {
}
pub struct GitPanel {
+ weak_workspace: WeakView<Workspace>,
current_modifiers: Modifiers,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
@@ -92,6 +96,7 @@ pub struct GitPanel {
all_staged: Option<bool>,
width: Option<Pixels>,
reveal_in_editor: Task<()>,
+ err_sender: mpsc::Sender<anyhow::Error>,
}
fn first_worktree_repository(
@@ -143,11 +148,14 @@ impl GitPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let fs = workspace.app_state().fs.clone();
let project = workspace.project().clone();
+ let weak_workspace = cx.view().downgrade();
let git_state = project.read(cx).git_state().cloned();
let language_registry = workspace.app_state().languages.clone();
let current_commit_message = git_state
.as_ref()
- .and_then(|git_state| git_state.read(cx).commit_message.clone());
+ .map(|git_state| git_state.read(cx).commit_message.clone());
+
+ let (err_sender, mut err_receiver) = mpsc::channel(1);
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
let focus_handle = cx.focus_handle();
@@ -319,6 +327,7 @@ impl GitPanel {
.detach();
let mut git_panel = Self {
+ weak_workspace,
focus_handle: cx.focus_handle(),
fs,
pending_serialization: Task::ready(None),
@@ -333,14 +342,33 @@ impl GitPanel {
hide_scrollbar_task: None,
rebuild_requested,
commit_editor,
- reveal_in_editor: Task::ready(()),
project,
+ reveal_in_editor: Task::ready(()),
+ err_sender,
};
git_panel.schedule_update();
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
git_panel
});
+ let handle = git_panel.downgrade();
+ cx.spawn(|_, mut cx| async move {
+ while let Some(e) = err_receiver.next().await {
+ let Some(this) = handle.upgrade() else {
+ break;
+ };
+ if this
+ .update(&mut cx, |this, cx| {
+ this.show_err_toast("git operation error", e, cx);
+ })
+ .is_err()
+ {
+ break;
+ }
+ }
+ })
+ .detach();
+
cx.subscribe(
&git_panel,
move |workspace, _, event: &Event, cx| match event.clone() {
@@ -606,13 +634,16 @@ impl GitPanel {
let Some(git_state) = self.git_state(cx) else {
return;
};
- git_state.update(cx, |git_state, _| {
+ let result = git_state.update(cx, |git_state, _| {
if entry.status.is_staged().unwrap_or(false) {
- git_state.stage_entries(vec![entry.repo_path.clone()]);
+ git_state.unstage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
} else {
- git_state.stage_entries(vec![entry.repo_path.clone()]);
+ git_state.stage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
}
});
+ if let Err(e) = result {
+ self.show_err_toast("toggle staged error", e, cx);
+ }
cx.notify();
}
@@ -649,7 +680,10 @@ impl GitPanel {
entry.is_staged = Some(true);
}
self.all_staged = Some(true);
- git_state.read(cx).stage_all();
+
+ if let Err(e) = git_state.read(cx).stage_all(self.err_sender.clone()) {
+ self.show_err_toast("stage all error", e, cx);
+ };
}
fn unstage_all(&mut self, _: &git::UnstageAll, cx: &mut ViewContext<Self>) {
@@ -660,7 +694,9 @@ impl GitPanel {
entry.is_staged = Some(false);
}
self.all_staged = Some(false);
- git_state.read(cx).unstage_all();
+ if let Err(e) = git_state.read(cx).unstage_all(self.err_sender.clone()) {
+ self.show_err_toast("unstage all error", e, cx);
+ };
}
fn discard_all(&mut self, _: &git::RevertAll, _cx: &mut ViewContext<Self>) {
@@ -668,53 +704,32 @@ impl GitPanel {
println!("Discard all triggered");
}
- fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
+ /// Commit all staged changes
+ fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext<Self>) {
let Some(git_state) = self.git_state(cx) else {
return;
};
- git_state.update(cx, |git_state, _| {
- git_state.clear_commit_message();
- });
+ if let Err(e) =
+ git_state.update(cx, |git_state, _| git_state.commit(self.err_sender.clone()))
+ {
+ self.show_err_toast("commit error", e, cx);
+ };
self.commit_editor
.update(cx, |editor, cx| editor.set_text("", cx));
}
- fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool {
- let Some(git_state) = self.git_state(cx) else {
- return false;
- };
- let has_message = !self.commit_editor.read(cx).text(cx).is_empty();
- let has_changes = git_state.read(cx).entry_count() > 0;
- let has_staged_changes = self
- .visible_entries
- .iter()
- .any(|entry| entry.is_staged == Some(true));
-
- has_message && (commit_all || has_staged_changes) && has_changes
- }
-
- /// Commit all staged changes
- fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext<Self>) {
- self.clear_message(cx);
-
- if !self.can_commit(false, cx) {
- return;
- }
-
- // TODO: Implement commit all staged
- println!("Commit staged changes triggered");
- }
-
/// Commit all changes, regardless of whether they are staged or not
fn commit_all_changes(&mut self, _: &git::CommitAllChanges, cx: &mut ViewContext<Self>) {
- self.clear_message(cx);
-
- if !self.can_commit(true, cx) {
+ let Some(git_state) = self.git_state(cx) else {
return;
- }
-
- // TODO: Implement commit all changes
- println!("Commit all changes triggered");
+ };
+ if let Err(e) = git_state.update(cx, |git_state, _| {
+ git_state.commit_all(self.err_sender.clone())
+ }) {
+ self.show_err_toast("commit all error", e, cx);
+ };
+ self.commit_editor
+ .update(cx, |editor, cx| editor.set_text("", cx));
}
fn no_entries(&self, cx: &mut ViewContext<Self>) -> bool {
@@ -840,12 +855,26 @@ impl GitPanel {
return;
};
git_state.update(cx, |git_state, _| {
- git_state.commit_message = Some(commit_message.into())
+ git_state.commit_message = commit_message.into();
});
cx.notify();
}
}
+
+ fn show_err_toast(&self, id: &'static str, e: anyhow::Error, cx: &mut ViewContext<Self>) {
+ let Some(workspace) = self.weak_workspace.upgrade() else {
+ return;
+ };
+ let notif_id = NotificationId::Named(id.into());
+ let message = e.to_string();
+ workspace.update(cx, |workspace, cx| {
+ let toast = Toast::new(notif_id, message).on_click("Open Zed Log", |cx| {
+ cx.dispatch_action(workspace::OpenLog.boxed_clone());
+ });
+ workspace.show_toast(toast, cx);
+ });
+ }
}
// GitPanel –– Render
@@ -989,6 +1018,10 @@ impl GitPanel {
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let editor = self.commit_editor.clone();
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
+ let (can_commit, can_commit_all) = self.git_state(cx).map_or((false, false), |git_state| {
+ let git_state = git_state.read(cx);
+ (git_state.can_commit(false), git_state.can_commit(true))
+ });
let focus_handle_1 = self.focus_handle(cx).clone();
let focus_handle_2 = self.focus_handle(cx).clone();
@@ -1004,6 +1037,7 @@ impl GitPanel {
cx,
)
})
+ .disabled(!can_commit)
.on_click(
cx.listener(|this, _: &ClickEvent, cx| this.commit_changes(&CommitChanges, cx)),
);
@@ -1019,6 +1053,7 @@ impl GitPanel {
cx,
)
})
+ .disabled(!can_commit_all)
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.commit_all_changes(&CommitAllChanges, cx)
}));
@@ -1243,14 +1278,15 @@ impl GitPanel {
let Some(git_state) = this.git_state(cx) else {
return;
};
- git_state.update(cx, |git_state, _| match toggle {
- ToggleState::Selected | ToggleState::Indeterminate => {
- git_state.stage_entries(vec![repo_path]);
- }
- ToggleState::Unselected => {
- git_state.unstage_entries(vec![repo_path])
- }
- })
+ let result = git_state.update(cx, |git_state, _| match toggle {
+ ToggleState::Selected | ToggleState::Indeterminate => git_state
+ .stage_entries(vec![repo_path], this.err_sender.clone()),
+ ToggleState::Unselected => git_state
+ .unstage_entries(vec![repo_path], this.err_sender.clone()),
+ });
+ if let Err(e) = result {
+ this.show_err_toast("toggle staged error", e, cx);
+ }
});
}
}),
@@ -1,52 +1,65 @@
use std::sync::Arc;
+use anyhow::anyhow;
use futures::channel::mpsc;
-use futures::StreamExt as _;
-use git::repository::{GitRepository, RepoPath};
+use futures::{SinkExt as _, StreamExt as _};
+use git::{
+ repository::{GitRepository, RepoPath},
+ status::{GitSummary, TrackedSummary},
+};
use gpui::{AppContext, SharedString};
use settings::WorktreeId;
-use util::ResultExt as _;
use worktree::RepositoryEntry;
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum StatusAction {
- Stage,
- Unstage,
-}
-
pub struct GitState {
/// The current commit message being composed.
- pub commit_message: Option<SharedString>,
+ pub commit_message: SharedString,
/// When a git repository is selected, this is used to track which repository's changes
/// are currently being viewed or modified in the UI.
pub active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
- pub update_sender: mpsc::UnboundedSender<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>,
+ update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
+}
+
+enum Message {
+ StageAndCommit(Arc<dyn GitRepository>, SharedString, Vec<RepoPath>),
+ Commit(Arc<dyn GitRepository>, SharedString),
+ Stage(Arc<dyn GitRepository>, Vec<RepoPath>),
+ Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
}
impl GitState {
pub fn new(cx: &AppContext) -> Self {
- let (tx, mut rx) =
- mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>();
+ let (update_sender, mut update_receiver) =
+ mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
cx.spawn(|cx| async move {
- while let Some((git_repo, paths, action)) = rx.next().await {
- cx.background_executor()
+ while let Some((msg, mut err_sender)) = update_receiver.next().await {
+ let result = cx
+ .background_executor()
.spawn(async move {
- match action {
- StatusAction::Stage => git_repo.stage_paths(&paths),
- StatusAction::Unstage => git_repo.unstage_paths(&paths),
+ match msg {
+ Message::StageAndCommit(repo, message, paths) => {
+ repo.stage_paths(&paths)?;
+ repo.commit(&message)?;
+ Ok(())
+ }
+ Message::Stage(repo, paths) => repo.stage_paths(&paths),
+ Message::Unstage(repo, paths) => repo.unstage_paths(&paths),
+ Message::Commit(repo, message) => repo.commit(&message),
}
})
- .await
- .log_err();
+ .await;
+ if let Err(e) = result {
+ err_sender.send(e).await.ok();
+ }
}
})
.detach();
GitState {
- commit_message: None,
+ commit_message: SharedString::default(),
active_repository: None,
- update_sender: tx,
+ update_sender,
}
}
@@ -65,55 +78,64 @@ impl GitState {
self.active_repository.as_ref()
}
- pub fn commit_message(&mut self, message: Option<SharedString>) {
- self.commit_message = message;
- }
-
- pub fn clear_commit_message(&mut self) {
- self.commit_message = None;
- }
-
- fn act_on_entries(&self, entries: Vec<RepoPath>, action: StatusAction) {
+ pub fn stage_entries(
+ &self,
+ entries: Vec<RepoPath>,
+ err_sender: mpsc::Sender<anyhow::Error>,
+ ) -> anyhow::Result<()> {
if entries.is_empty() {
- return;
- }
- if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
- let _ = self
- .update_sender
- .unbounded_send((git_repo.clone(), entries, action));
+ return Ok(());
}
+ let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
+ return Err(anyhow!("No active repository"));
+ };
+ self.update_sender
+ .unbounded_send((Message::Stage(git_repo.clone(), entries), err_sender))
+ .map_err(|_| anyhow!("Failed to submit stage operation"))?;
+ Ok(())
}
- pub fn stage_entries(&self, entries: Vec<RepoPath>) {
- self.act_on_entries(entries, StatusAction::Stage);
- }
-
- pub fn unstage_entries(&self, entries: Vec<RepoPath>) {
- self.act_on_entries(entries, StatusAction::Unstage);
+ pub fn unstage_entries(
+ &self,
+ entries: Vec<RepoPath>,
+ err_sender: mpsc::Sender<anyhow::Error>,
+ ) -> anyhow::Result<()> {
+ if entries.is_empty() {
+ return Ok(());
+ }
+ let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
+ return Err(anyhow!("No active repository"));
+ };
+ self.update_sender
+ .unbounded_send((Message::Unstage(git_repo.clone(), entries), err_sender))
+ .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
+ Ok(())
}
- pub fn stage_all(&self) {
+ pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
let Some((_, entry, _)) = self.active_repository.as_ref() else {
- return;
+ return Err(anyhow!("No active repository"));
};
let to_stage = entry
.status()
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
.map(|entry| entry.repo_path.clone())
.collect();
- self.stage_entries(to_stage);
+ self.stage_entries(to_stage, err_sender)?;
+ Ok(())
}
- pub fn unstage_all(&self) {
+ pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
let Some((_, entry, _)) = self.active_repository.as_ref() else {
- return;
+ return Err(anyhow!("No active repository"));
};
let to_unstage = entry
.status()
.filter(|entry| entry.status.is_staged().unwrap_or(true))
.map(|entry| entry.repo_path.clone())
.collect();
- self.unstage_entries(to_unstage);
+ self.unstage_entries(to_unstage, err_sender)?;
+ Ok(())
}
/// Get a count of all entries in the active repository, including
@@ -123,4 +145,61 @@ impl GitState {
.as_ref()
.map_or(0, |(_, entry, _)| entry.status_len())
}
+
+ fn have_changes(&self) -> bool {
+ let Some((_, entry, _)) = self.active_repository.as_ref() else {
+ return false;
+ };
+ entry.status_summary() != GitSummary::UNCHANGED
+ }
+
+ fn have_staged_changes(&self) -> bool {
+ let Some((_, entry, _)) = self.active_repository.as_ref() else {
+ return false;
+ };
+ entry.status_summary().index != TrackedSummary::UNCHANGED
+ }
+
+ pub fn can_commit(&self, commit_all: bool) -> bool {
+ return !self.commit_message.trim().is_empty()
+ && self.have_changes()
+ && (commit_all || self.have_staged_changes());
+ }
+
+ pub fn commit(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
+ if !self.can_commit(false) {
+ return Err(anyhow!("Unable to commit"));
+ }
+ let Some((_, _, git_repo)) = self.active_repository() else {
+ return Err(anyhow!("No active repository"));
+ };
+ let git_repo = git_repo.clone();
+ let message = std::mem::take(&mut self.commit_message);
+ self.update_sender
+ .unbounded_send((Message::Commit(git_repo, message), err_sender))
+ .map_err(|_| anyhow!("Failed to submit commit operation"))?;
+ Ok(())
+ }
+
+ pub fn commit_all(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
+ if !self.can_commit(true) {
+ return Err(anyhow!("Unable to commit"));
+ }
+ let Some((_, entry, git_repo)) = self.active_repository.as_ref() else {
+ return Err(anyhow!("No active repository"));
+ };
+ let to_stage = entry
+ .status()
+ .filter(|entry| !entry.status.is_staged().unwrap_or(false))
+ .map(|entry| entry.repo_path.clone())
+ .collect::<Vec<_>>();
+ let message = std::mem::take(&mut self.commit_message);
+ self.update_sender
+ .unbounded_send((
+ Message::StageAndCommit(git_repo.clone(), message, to_stage),
+ err_sender,
+ ))
+ .map_err(|_| anyhow!("Failed to submit commit operation"))?;
+ Ok(())
+ }
}
@@ -1588,7 +1588,7 @@ impl ProjectPanel {
}
}))
&& entry.is_file()
- && entry.git_summary.modified > 0
+ && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
},
cx,
);
@@ -1666,7 +1666,7 @@ impl ProjectPanel {
}
}))
&& entry.is_file()
- && entry.git_summary.modified > 0
+ && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
},
cx,
);
@@ -231,6 +231,10 @@ impl RepositoryEntry {
self.statuses_by_path.summary().item_summary.count
}
+ pub fn status_summary(&self) -> GitSummary {
+ self.statuses_by_path.summary().item_summary
+ }
+
pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
self.statuses_by_path
.get(&PathKey(path.0.clone()), &())
@@ -6,7 +6,8 @@ use anyhow::Result;
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
use git::{
status::{
- FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
+ FileStatus, GitSummary, StatusCode, TrackedStatus, TrackedSummary, UnmergedStatus,
+ UnmergedStatusCode,
},
GITIGNORE,
};
@@ -745,7 +746,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
Path::new("/root/tree/.git"),
&[(
Path::new("tracked-dir/tracked-file2"),
- FileStatus::worktree(StatusCode::Added),
+ StatusCode::Added.index(),
)],
);
@@ -830,7 +831,7 @@ async fn test_update_gitignore(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_working_copy_change(
Path::new("/root/.git"),
- &[(Path::new("b.txt"), FileStatus::worktree(StatusCode::Added))],
+ &[(Path::new("b.txt"), StatusCode::Added.index())],
);
cx.executor().run_until_parked();
@@ -1500,10 +1501,7 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
// detected.
fs.set_status_for_repo_via_git_operation(
Path::new("/root/.git"),
- &[(
- Path::new("b/c.txt"),
- FileStatus::worktree(StatusCode::Modified),
- )],
+ &[(Path::new("b/c.txt"), StatusCode::Modified.index())],
);
cx.executor().run_until_parked();
@@ -2199,7 +2197,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
assert_eq!(
tree.status_for_file(Path::new("projects/project1/a")),
- Some(FileStatus::worktree(StatusCode::Modified)),
+ Some(StatusCode::Modified.worktree()),
);
assert_eq!(
tree.status_for_file(Path::new("projects/project1/b")),
@@ -2220,7 +2218,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
assert_eq!(
tree.status_for_file(Path::new("projects/project2/a")),
- Some(FileStatus::worktree(StatusCode::Modified)),
+ Some(StatusCode::Modified.worktree()),
);
assert_eq!(
tree.status_for_file(Path::new("projects/project2/b")),
@@ -2421,7 +2419,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(A_TXT)),
- Some(FileStatus::worktree(StatusCode::Modified)),
+ Some(StatusCode::Modified.worktree()),
);
});
@@ -2463,7 +2461,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
);
assert_eq!(
snapshot.status_for_file(project_path.join(E_TXT)),
- Some(FileStatus::worktree(StatusCode::Modified)),
+ Some(StatusCode::Modified.worktree()),
);
});
@@ -2575,14 +2573,11 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
- assert_eq!(
- entries[0].status,
- FileStatus::worktree(StatusCode::Modified)
- );
+ assert_eq!(entries[0].status, StatusCode::Modified.worktree());
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, FileStatus::Untracked);
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
- assert_eq!(entries[2].status, FileStatus::worktree(StatusCode::Deleted));
+ assert_eq!(entries[2].status, StatusCode::Deleted.worktree());
});
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
@@ -2600,20 +2595,14 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
- assert_eq!(
- entries[0].status,
- FileStatus::worktree(StatusCode::Modified)
- );
+ assert_eq!(entries[0].status, StatusCode::Modified.worktree());
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, FileStatus::Untracked);
// Status updated
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
- assert_eq!(
- entries[2].status,
- FileStatus::worktree(StatusCode::Modified)
- );
+ assert_eq!(entries[2].status, StatusCode::Modified.worktree());
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
- assert_eq!(entries[3].status, FileStatus::worktree(StatusCode::Deleted));
+ assert_eq!(entries[3].status, StatusCode::Deleted.worktree());
});
git_add("a.txt", &repo);
@@ -2646,7 +2635,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
&entries
);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
- assert_eq!(entries[0].status, FileStatus::worktree(StatusCode::Deleted));
+ assert_eq!(entries[0].status, StatusCode::Deleted.worktree());
});
}
@@ -2769,11 +2758,8 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[
- (
- Path::new("x2.txt"),
- FileStatus::worktree(StatusCode::Modified),
- ),
- (Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
+ (Path::new("x2.txt"), StatusCode::Modified.index()),
+ (Path::new("z.txt"), StatusCode::Added.index()),
],
);
fs.set_status_for_repo_via_git_operation(
@@ -2782,7 +2768,7 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
- &[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
+ &[(Path::new("z2.txt"), StatusCode::Added.index())],
);
let tree = Worktree::local(
@@ -2862,14 +2848,8 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/.git"),
&[
- (
- Path::new("a/b/c1.txt"),
- FileStatus::worktree(StatusCode::Added),
- ),
- (
- Path::new("a/d/e2.txt"),
- FileStatus::worktree(StatusCode::Modified),
- ),
+ (Path::new("a/b/c1.txt"), StatusCode::Added.index()),
+ (Path::new("a/d/e2.txt"), StatusCode::Modified.index()),
(Path::new("g/h2.txt"), CONFLICT),
],
);
@@ -2971,24 +2951,18 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
- &[(Path::new("x1.txt"), FileStatus::worktree(StatusCode::Added))],
+ &[(Path::new("x1.txt"), StatusCode::Added.index())],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/y/.git"),
&[
(Path::new("y1.txt"), CONFLICT),
- (
- Path::new("y2.txt"),
- FileStatus::worktree(StatusCode::Modified),
- ),
+ (Path::new("y2.txt"), StatusCode::Modified.index()),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
- &[(
- Path::new("z2.txt"),
- FileStatus::worktree(StatusCode::Modified),
- )],
+ &[(Path::new("z2.txt"), StatusCode::Modified.index())],
);
let tree = Worktree::local(
@@ -3081,11 +3055,8 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[
- (
- Path::new("x2.txt"),
- FileStatus::worktree(StatusCode::Modified),
- ),
- (Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
+ (Path::new("x2.txt"), StatusCode::Modified.index()),
+ (Path::new("z.txt"), StatusCode::Added.index()),
],
);
fs.set_status_for_repo_via_git_operation(
@@ -3095,7 +3066,7 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
- &[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
+ &[(Path::new("z2.txt"), StatusCode::Added.index())],
);
let tree = Worktree::local(
@@ -3227,12 +3198,12 @@ fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSumma
}
const ADDED: GitSummary = GitSummary {
- added: 1,
+ index: TrackedSummary::ADDED,
count: 1,
..GitSummary::UNCHANGED
};
const MODIFIED: GitSummary = GitSummary {
- modified: 1,
+ index: TrackedSummary::MODIFIED,
count: 1,
..GitSummary::UNCHANGED
};
@@ -3378,15 +3349,15 @@ fn init_test(cx: &mut gpui::TestAppContext) {
fn assert_entry_git_state(
tree: &Worktree,
path: &str,
- worktree_status: Option<StatusCode>,
+ index_status: Option<StatusCode>,
is_ignored: bool,
) {
let entry = tree.entry_for_path(path).expect("entry {path} not found");
let status = tree.status_for_file(Path::new(path));
- let expected = worktree_status.map(|worktree_status| {
+ let expected = index_status.map(|index_status| {
TrackedStatus {
- worktree_status,
- index_status: StatusCode::Unmodified,
+ index_status,
+ worktree_status: StatusCode::Unmodified,
}
.into()
});