diff --git a/Cargo.lock b/Cargo.lock index 2db71904af6c4f8a09fa7c03593b967a7ea94236..2a21a0fdbdb7051d822dc5b5892b46dbf8df47dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5192,6 +5192,7 @@ dependencies = [ "collections", "db", "editor", + "futures 0.3.31", "git", "gpui", "language", diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 4b4dd50f7f98e34fc48e542d49ab776ae1944631..9931685035b080270323edb2b0a0da88b3f3c3f2 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 11a76b0dd89500e83783aa464d0f225da209ddbf..e4e9af3ff634d822e981b3e48e7262bbd3a5790a 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -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<()> { diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index 1c8b6e757ad01d74bd58d8e4b16d8be0b51be9b4..ea3af140afd45f04fd86d938792bb52fc6d3e661 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -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 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, } } diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 2b8c84370140bd1ead14ab7c9c3fc4b46f3f72e2..a8cf91d9575e033cb24113f2e300b4c31a9832db 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -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 diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 08f6ba6058d339362aaffd4b642a0000a3a1fff1..61dd8f2927d11133f29370b1fa37f2b35dfd49e8 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -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, current_modifiers: Modifiers, focus_handle: FocusHandle, fs: Arc, @@ -92,6 +96,7 @@ pub struct GitPanel { all_staged: Option, width: Option, reveal_in_editor: Task<()>, + err_sender: mpsc::Sender, } fn first_worktree_repository( @@ -143,11 +148,14 @@ impl GitPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { 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| { 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) { @@ -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) { @@ -668,53 +704,32 @@ impl GitPanel { println!("Discard all triggered"); } - fn clear_message(&mut self, cx: &mut ViewContext) { + /// Commit all staged changes + fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext) { 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.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.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) -> 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) { + 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) -> 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); + } }); } }), diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index cbb7ed90dc4ccaa6b3e14ec26c49fe879be89751..db569e3e28e8de3724b330795d7ab3b988d6a576 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -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, + 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)>, - pub update_sender: mpsc::UnboundedSender<(Arc, Vec, StatusAction)>, + update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender)>, +} + +enum Message { + StageAndCommit(Arc, SharedString, Vec), + Commit(Arc, SharedString), + Stage(Arc, Vec), + Unstage(Arc, Vec), } impl GitState { pub fn new(cx: &AppContext) -> Self { - let (tx, mut rx) = - mpsc::unbounded::<(Arc, Vec, StatusAction)>(); + let (update_sender, mut update_receiver) = + mpsc::unbounded::<(Message, mpsc::Sender)>(); 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) { - self.commit_message = message; - } - - pub fn clear_commit_message(&mut self) { - self.commit_message = None; - } - - fn act_on_entries(&self, entries: Vec, action: StatusAction) { + pub fn stage_entries( + &self, + entries: Vec, + err_sender: mpsc::Sender, + ) -> 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) { - self.act_on_entries(entries, StatusAction::Stage); - } - - pub fn unstage_entries(&self, entries: Vec) { - self.act_on_entries(entries, StatusAction::Unstage); + pub fn unstage_entries( + &self, + entries: Vec, + err_sender: mpsc::Sender, + ) -> 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::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::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::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::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::>(); + 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(()) + } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 28335f5f8d6d505f2104b5f887e12689202a67e1..e6b72d86fdd9d542a5f348279947c7b32d964584 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -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, ); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 2acbc15adbeedc7adbccd42b30e0432948aa4c86..ea2d8b62eb9bbce34b1c61b8f6d1297984e1dd25 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -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 { self.statuses_by_path .get(&PathKey(path.0.clone()), &()) diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 34c37626dbe0322bc6865d550e0c4532a974be63..c47fb4318056e102e693fbf7c345bcd4e1d62d85 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -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, + index_status: Option, 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() });