diff --git a/Cargo.lock b/Cargo.lock index 22da73b14c..4abb09fc7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5129,7 +5129,6 @@ dependencies = [ "collections", "db", "editor", - "futures 0.3.31", "git", "gpui", "language", @@ -5140,7 +5139,6 @@ dependencies = [ "serde_derive", "serde_json", "settings", - "sum_tree", "theme", "ui", "util", diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 8df011e7dd..c9e41fd41e 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2942,7 +2942,7 @@ pub mod tests { .update(cx, |editor, cx| { assert_eq!( vec!["main hint #0".to_string(), "other hint #0".to_string()], - cached_hint_labels(editor), + sorted_cached_hint_labels(editor), "Cache should update for both excerpts despite hints display was disabled" ); assert!( diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index c608c23cf3..f300c21cf9 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -7,6 +7,7 @@ pub mod repository; pub mod status; use anyhow::{anyhow, Context, Result}; +use gpui::actions; use serde::{Deserialize, Serialize}; use std::ffi::OsStr; use std::fmt; @@ -24,6 +25,24 @@ pub static FSMONITOR_DAEMON: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("fsmonitor--daemon")); pub static GITIGNORE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".gitignore")); +actions!( + git, + [ + StageFile, + UnstageFile, + ToggleStaged, + // Revert actions are currently in the editor crate: + // editor::RevertFile, + // editor::RevertSelectedHunks + StageAll, + UnstageAll, + RevertAll, + CommitChanges, + CommitAllChanges, + ClearCommitMessage + ] +); + #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Oid(libgit::Oid); diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 0c357cb436..120ca92857 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -17,7 +17,6 @@ anyhow.workspace = true collections.workspace = true db.workspace = true editor.workspace = true -futures.workspace = true git.workspace = true gpui.workspace = true language.workspace = true @@ -28,7 +27,6 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -sum_tree.workspace = true theme.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index bc47c2d640..771113cf40 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1,18 +1,17 @@ use crate::git_panel_settings::StatusStyle; -use crate::{first_repository_in_project, first_worktree_repository}; -use crate::{ - git_panel_settings::GitPanelSettings, git_status_icon, CommitAllChanges, CommitChanges, - GitState, GitViewMode, RevertAll, StageAll, ToggleStaged, UnstageAll, -}; +use crate::{git_panel_settings::GitPanelSettings, git_status_icon}; use anyhow::{Context as _, Result}; use db::kvp::KEY_VALUE_STORE; use editor::scroll::ScrollbarAutoHide; use editor::{Editor, EditorSettings, ShowScrollbar}; -use git::{repository::RepoPath, status::FileStatus}; +use git::repository::{GitRepository, RepoPath}; +use git::status::FileStatus; +use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll}; use gpui::*; use language::Buffer; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::{Fs, Project, ProjectPath}; +use project::git::GitState; +use project::{Fs, Project, ProjectPath, WorktreeId}; use serde::{Deserialize, Serialize}; use settings::Settings as _; use std::sync::atomic::{AtomicBool, Ordering}; @@ -21,12 +20,13 @@ use theme::ThemeSettings; use ui::{ prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip, }; -use util::{ResultExt, TryFutureExt}; +use util::{maybe, ResultExt, TryFutureExt}; use workspace::notifications::DetachAndPromptErr; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; +use worktree::RepositoryEntry; actions!( git_panel, @@ -87,7 +87,6 @@ pub struct GitPanel { selected_entry: Option, show_scrollbar: bool, rebuild_requested: Arc, - git_state: GitState, commit_editor: View, /// The visible entries in the list, accounting for folding & expanded state. /// @@ -99,6 +98,44 @@ pub struct GitPanel { reveal_in_editor: Task<()>, } +fn first_worktree_repository( + project: &Model, + worktree_id: WorktreeId, + cx: &mut AppContext, +) -> Option<(RepositoryEntry, Arc)> { + project + .read(cx) + .worktree_for_id(worktree_id, cx) + .and_then(|worktree| { + let snapshot = worktree.read(cx).snapshot(); + let repo = snapshot.repositories().iter().next()?.clone(); + let git_repo = worktree + .read(cx) + .as_local()? + .get_local_repo(&repo)? + .repo() + .clone(); + Some((repo, git_repo)) + }) +} + +fn first_repository_in_project( + project: &Model, + cx: &mut AppContext, +) -> Option<(WorktreeId, RepositoryEntry, Arc)> { + project.read(cx).worktrees(cx).next().and_then(|worktree| { + let snapshot = worktree.read(cx).snapshot(); + let repo = snapshot.repositories().iter().next()?.clone(); + let git_repo = worktree + .read(cx) + .as_local()? + .get_local_repo(&repo)? + .repo() + .clone(); + Some((snapshot.id(), repo, git_repo)) + }) +} + impl GitPanel { pub fn load( workspace: WeakView, @@ -110,9 +147,11 @@ 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 git_state = project.read(cx).git_state().cloned(); let language_registry = workspace.app_state().languages.clone(); - let mut git_state = GitState::new(cx); - let current_commit_message = git_state.commit_message.clone(); + let current_commit_message = git_state + .as_ref() + .and_then(|git_state| git_state.read(cx).commit_message.clone()); let git_panel = cx.new_view(|cx: &mut ViewContext| { let focus_handle = cx.focus_handle(); @@ -124,82 +163,78 @@ impl GitPanel { cx.subscribe(&project, move |this, project, event, cx| { use project::Event; - let git_state = &mut this.git_state; let first_worktree_id = project.read(cx).worktrees(cx).next().map(|worktree| { let snapshot = worktree.read(cx).snapshot(); snapshot.id() }); let first_repo_in_project = first_repository_in_project(&project, cx); - match event { - project::Event::WorktreeRemoved(id) => { - git_state.all_repositories.remove(id); - let Some((worktree_id, _, _)) = git_state.active_repository.as_ref() else { - return; - }; - if worktree_id == id { - git_state.active_repository = first_repo_in_project; - this.schedule_update(); - } - } - project::Event::WorktreeOrderChanged => { - // activate the new first worktree if the first was moved - let Some(first_id) = first_worktree_id else { - return; - }; - if !git_state - .active_repository - .as_ref() - .is_some_and(|(id, _, _)| id == &first_id) - { - git_state.active_repository = first_repo_in_project; - this.schedule_update(); - } - } - Event::WorktreeAdded(id) => { - let Some(worktree) = project.read(cx).worktree_for_id(*id, cx) else { - return; - }; - let snapshot = worktree.read(cx).snapshot(); - git_state - .all_repositories - .insert(*id, snapshot.repositories().clone()); - let Some(first_id) = first_worktree_id else { - return; - }; - if !git_state - .active_repository - .as_ref() - .is_some_and(|(id, _, _)| id == &first_id) - { - git_state.active_repository = first_repo_in_project; - this.schedule_update(); - } - } - project::Event::WorktreeUpdatedEntries(id, _) => { - if git_state - .active_repository - .as_ref() - .is_some_and(|(active_id, _, _)| active_id == id) - { - git_state.active_repository = first_repo_in_project; - this.schedule_update(); - } - } - project::Event::WorktreeUpdatedGitRepositories(_) => { - let Some(first) = first_repo_in_project else { - return; - }; - git_state.active_repository = Some(first); - this.schedule_update(); - } - project::Event::Closed => { - this.reveal_in_editor = Task::ready(()); - this.visible_entries.clear(); - // TODO cancel/clear task? - } - _ => {} + let Some(git_state) = project.read(cx).git_state().cloned() else { + return; }; + git_state.update(cx, |git_state, _| { + match event { + project::Event::WorktreeRemoved(id) => { + let Some((worktree_id, _, _)) = git_state.active_repository.as_ref() + else { + return; + }; + if worktree_id == id { + git_state.active_repository = first_repo_in_project; + this.schedule_update(); + } + } + project::Event::WorktreeOrderChanged => { + // activate the new first worktree if the first was moved + let Some(first_id) = first_worktree_id else { + return; + }; + if !git_state + .active_repository + .as_ref() + .is_some_and(|(id, _, _)| id == &first_id) + { + git_state.active_repository = first_repo_in_project; + this.schedule_update(); + } + } + Event::WorktreeAdded(_) => { + let Some(first_id) = first_worktree_id else { + return; + }; + if !git_state + .active_repository + .as_ref() + .is_some_and(|(id, _, _)| id == &first_id) + { + git_state.active_repository = first_repo_in_project; + this.schedule_update(); + } + } + project::Event::WorktreeUpdatedEntries(id, _) => { + if git_state + .active_repository + .as_ref() + .is_some_and(|(active_id, _, _)| active_id == id) + { + git_state.active_repository = first_repo_in_project; + this.schedule_update(); + } + } + project::Event::WorktreeUpdatedGitRepositories(_) => { + let Some(first) = first_repo_in_project else { + return; + }; + git_state.active_repository = Some(first); + this.schedule_update(); + } + project::Event::Closed => { + this.reveal_in_editor = Task::ready(()); + this.visible_entries.clear(); + } + _ => {} + }; + }); }) .detach(); @@ -259,10 +294,12 @@ impl GitPanel { if let Some(first_worktree) = first_worktree { let snapshot = first_worktree.read(cx).snapshot(); - if let Some((repo, git_repo)) = - first_worktree_repository(&project, snapshot.id(), cx) + if let Some(((repo, git_repo), git_state)) = + first_worktree_repository(&project, snapshot.id(), cx).zip(git_state) { - git_state.activate_repository(snapshot.id(), repo, git_repo); + git_state.update(cx, |git_state, _| { + git_state.activate_repository(snapshot.id(), repo, git_repo); + }); } }; @@ -300,7 +337,6 @@ impl GitPanel { hide_scrollbar_task: None, rebuild_requested, commit_editor, - git_state, reveal_in_editor: Task::ready(()), project, }; @@ -327,6 +363,19 @@ impl GitPanel { git_panel } + fn git_state<'a>(&self, cx: &'a AppContext) -> Option<&'a Model> { + self.project.read(cx).git_state() + } + + fn active_repository<'a>( + &self, + cx: &'a AppContext, + ) -> Option<&'a (WorktreeId, RepositoryEntry, Arc)> { + let git_state = self.git_state(cx)?; + let active_repository = git_state.read(cx).active_repository.as_ref()?; + Some(active_repository) + } + fn serialize(&mut self, cx: &mut ViewContext) { // TODO: we can store stage status here let width = self.width; @@ -549,14 +598,20 @@ impl GitPanel { } fn toggle_staged_for_entry(&mut self, entry: &GitListEntry, cx: &mut ViewContext) { - match entry.status.is_staged() { - Some(true) | None => self.git_state.unstage_entry(entry.repo_path.clone()), - Some(false) => self.git_state.stage_entry(entry.repo_path.clone()), - } + let Some(git_state) = self.git_state(cx).cloned() else { + return; + }; + git_state.update(cx, |git_state, _| { + if entry.status.is_staged().unwrap_or(false) { + git_state.unstage_entry(entry.repo_path.clone()); + } else { + git_state.stage_entry(entry.repo_path.clone()); + } + }); cx.notify(); } - fn toggle_staged_for_selected(&mut self, _: &ToggleStaged, cx: &mut ViewContext) { + fn toggle_staged_for_selected(&mut self, _: &git::ToggleStaged, cx: &mut ViewContext) { if let Some(selected_entry) = self.get_selected_entry().cloned() { self.toggle_staged_for_entry(&selected_entry, cx); } @@ -572,14 +627,12 @@ impl GitPanel { } fn open_entry(&self, entry: &GitListEntry, cx: &mut ViewContext) { - let Some((worktree_id, path)) = - self.git_state - .active_repository - .as_ref() - .and_then(|(id, repo, _)| { - Some((*id, repo.work_directory.unrelativize(&entry.repo_path)?)) - }) - else { + let Some((worktree_id, path)) = maybe!({ + let git_state = self.git_state(cx)?; + let (id, repo, _) = git_state.read(cx).active_repository.as_ref()?; + let path = repo.work_directory.unrelativize(&entry.repo_path)?; + Some((*id, path)) + }) else { return; }; let path = (worktree_id, path).into(); @@ -592,7 +645,7 @@ impl GitPanel { cx.emit(Event::OpenedEntry { path }); } - fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext) { + fn stage_all(&mut self, _: &git::StageAll, cx: &mut ViewContext) { let to_stage = self .visible_entries .iter_mut() @@ -603,31 +656,42 @@ impl GitPanel { }) .collect(); self.all_staged = Some(true); - self.git_state.stage_entries(to_stage); + let Some(git_state) = self.git_state(cx).cloned() else { + return; + }; + git_state.update(cx, |git_state, _| git_state.stage_entries(to_stage)); } - fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext) { + fn unstage_all(&mut self, _: &git::UnstageAll, cx: &mut ViewContext) { // This should only be called when all entries are staged. for entry in &mut self.visible_entries { entry.is_staged = Some(false); } self.all_staged = Some(false); - self.git_state.unstage_all(); + let Some(git_state) = self.git_state(cx).cloned() else { + return; + }; + git_state.update(cx, |git_state, _| git_state.unstage_all()); } - fn discard_all(&mut self, _: &RevertAll, _cx: &mut ViewContext) { + fn discard_all(&mut self, _: &git::RevertAll, _cx: &mut ViewContext) { // TODO: Implement discard all println!("Discard all triggered"); } fn clear_message(&mut self, cx: &mut ViewContext) { - self.git_state.clear_commit_message(); + let Some(git_state) = self.git_state(cx).cloned() else { + return; + }; + git_state.update(cx, |git_state, _| { + git_state.clear_commit_message(); + }); self.commit_editor .update(cx, |editor, cx| editor.set_text("", cx)); } /// Commit all staged changes - fn commit_changes(&mut self, _: &CommitChanges, cx: &mut ViewContext) { + fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext) { self.clear_message(cx); // TODO: Implement commit all staged @@ -635,7 +699,7 @@ impl GitPanel { } /// Commit all changes, regardless of whether they are staged or not - fn commit_all_changes(&mut self, _: &CommitAllChanges, cx: &mut ViewContext) { + fn commit_all_changes(&mut self, _: &git::CommitAllChanges, cx: &mut ViewContext) { self.clear_message(cx); // TODO: Implement commit all changes @@ -691,7 +755,7 @@ impl GitPanel { fn update_visible_entries(&mut self, cx: &mut ViewContext) { self.visible_entries.clear(); - let Some((_, repo, _)) = self.git_state.active_repository().as_ref() else { + let Some((_, repo, _)) = self.active_repository(cx) else { // Just clear entries if no repository is active. cx.notify(); return; @@ -764,7 +828,12 @@ impl GitPanel { if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event { let commit_message = self.commit_editor.update(cx, |editor, cx| editor.text(cx)); - self.git_state.commit_message = Some(commit_message.into()); + let Some(git_state) = self.git_state(cx).cloned() else { + return; + }; + git_state.update(cx, |git_state, _| { + git_state.commit_message = Some(commit_message.into()) + }); cx.notify(); } @@ -1094,7 +1163,7 @@ impl GitPanel { let entry_id = ElementId::Name(format!("entry_{}", entry_details.display_name).into()); let checkbox_id = ElementId::Name(format!("checkbox_{}", entry_details.display_name).into()); - let view_mode = self.git_state.list_view_mode; + let is_tree_view = false; let handle = cx.view().downgrade(); let end_slot = h_flex() @@ -1125,7 +1194,7 @@ impl GitPanel { this.hover(|this| this.bg(cx.theme().colors().ghost_element_hover)) }); - if view_mode == GitViewMode::Tree { + if is_tree_view { entry = entry.pl(px(8. + 12. * entry_details.depth as f32)) } else { entry = entry.pl(px(8.)) @@ -1152,19 +1221,22 @@ impl GitPanel { let Some(this) = handle.upgrade() else { return; }; - this.update(cx, |this, _| { + this.update(cx, |this, cx| { this.visible_entries[ix].is_staged = match *toggle { ToggleState::Selected => Some(true), ToggleState::Unselected => Some(false), ToggleState::Indeterminate => None, }; let repo_path = repo_path.clone(); - match toggle { + let Some(git_state) = this.git_state(cx).cloned() else { + return; + }; + git_state.update(cx, |git_state, _| match toggle { ToggleState::Selected | ToggleState::Indeterminate => { - this.git_state.stage_entry(repo_path); + git_state.stage_entry(repo_path); } - ToggleState::Unselected => this.git_state.unstage_entry(repo_path), - } + ToggleState::Unselected => git_state.unstage_entry(repo_path), + }) }); } }), diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index ba2a394402..5bbd1b4585 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -1,209 +1,16 @@ use ::settings::Settings; -use collections::HashMap; -use futures::channel::mpsc; -use futures::StreamExt as _; -use git::repository::{GitRepository, RepoPath}; use git::status::FileStatus; use git_panel_settings::GitPanelSettings; -use gpui::{actions, AppContext, Hsla, Model}; -use project::{Project, WorktreeId}; -use std::sync::Arc; -use sum_tree::SumTree; -use ui::{Color, Icon, IconName, IntoElement, SharedString}; -use util::ResultExt as _; -use worktree::RepositoryEntry; +use gpui::{AppContext, Hsla}; +use ui::{Color, Icon, IconName, IntoElement}; pub mod git_panel; mod git_panel_settings; -actions!( - git, - [ - StageFile, - UnstageFile, - ToggleStaged, - // Revert actions are currently in the editor crate: - // editor::RevertFile, - // editor::RevertSelectedHunks - StageAll, - UnstageAll, - RevertAll, - CommitChanges, - CommitAllChanges, - ClearCommitMessage - ] -); - pub fn init(cx: &mut AppContext) { GitPanelSettings::register(cx); } -#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] -pub enum GitViewMode { - #[default] - List, - Tree, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum StatusAction { - Stage, - Unstage, -} - -pub struct GitState { - /// The current commit message being composed. - commit_message: Option, - - /// When a git repository is selected, this is used to track which repository's changes - /// are currently being viewed or modified in the UI. - active_repository: Option<(WorktreeId, RepositoryEntry, Arc)>, - - updater_tx: mpsc::UnboundedSender<(Arc, Vec, StatusAction)>, - - all_repositories: HashMap>, - - list_view_mode: GitViewMode, -} - -impl GitState { - pub fn new(cx: &AppContext) -> Self { - let (updater_tx, mut updater_rx) = - mpsc::unbounded::<(Arc, Vec, StatusAction)>(); - cx.spawn(|cx| async move { - while let Some((git_repo, paths, action)) = updater_rx.next().await { - cx.background_executor() - .spawn(async move { - match action { - StatusAction::Stage => git_repo.stage_paths(&paths), - StatusAction::Unstage => git_repo.unstage_paths(&paths), - } - }) - .await - .log_err(); - } - }) - .detach(); - GitState { - commit_message: None, - active_repository: None, - updater_tx, - list_view_mode: GitViewMode::default(), - all_repositories: HashMap::default(), - } - } - - pub fn activate_repository( - &mut self, - worktree_id: WorktreeId, - active_repository: RepositoryEntry, - git_repo: Arc, - ) { - self.active_repository = Some((worktree_id, active_repository, git_repo)); - } - - pub fn active_repository( - &self, - ) -> Option<&(WorktreeId, RepositoryEntry, Arc)> { - 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; - } - - pub fn stage_entry(&mut self, repo_path: RepoPath) { - if let Some((_, _, git_repo)) = self.active_repository.as_ref() { - let _ = self.updater_tx.unbounded_send(( - git_repo.clone(), - vec![repo_path], - StatusAction::Stage, - )); - } - } - - pub fn unstage_entry(&mut self, repo_path: RepoPath) { - if let Some((_, _, git_repo)) = self.active_repository.as_ref() { - let _ = self.updater_tx.unbounded_send(( - git_repo.clone(), - vec![repo_path], - StatusAction::Unstage, - )); - } - } - - pub fn stage_entries(&mut self, entries: Vec) { - if let Some((_, _, git_repo)) = self.active_repository.as_ref() { - let _ = - self.updater_tx - .unbounded_send((git_repo.clone(), entries, StatusAction::Stage)); - } - } - - fn act_on_all(&mut self, action: StatusAction) { - if let Some((_, active_repository, git_repo)) = self.active_repository.as_ref() { - let _ = self.updater_tx.unbounded_send(( - git_repo.clone(), - active_repository - .status() - .map(|entry| entry.repo_path) - .collect(), - action, - )); - } - } - - pub fn stage_all(&mut self) { - self.act_on_all(StatusAction::Stage); - } - - pub fn unstage_all(&mut self) { - self.act_on_all(StatusAction::Unstage); - } -} - -pub fn first_worktree_repository( - project: &Model, - worktree_id: WorktreeId, - cx: &mut AppContext, -) -> Option<(RepositoryEntry, Arc)> { - project - .read(cx) - .worktree_for_id(worktree_id, cx) - .and_then(|worktree| { - let snapshot = worktree.read(cx).snapshot(); - let repo = snapshot.repositories().iter().next()?.clone(); - let git_repo = worktree - .read(cx) - .as_local()? - .get_local_repo(&repo)? - .repo() - .clone(); - Some((repo, git_repo)) - }) -} - -pub fn first_repository_in_project( - project: &Model, - cx: &mut AppContext, -) -> Option<(WorktreeId, RepositoryEntry, Arc)> { - project.read(cx).worktrees(cx).next().and_then(|worktree| { - let snapshot = worktree.read(cx).snapshot(); - let repo = snapshot.repositories().iter().next()?.clone(); - let git_repo = worktree - .read(cx) - .as_local()? - .get_local_repo(&repo)? - .repo() - .clone(); - Some((snapshot.id(), repo, git_repo)) - }) -} - const ADDED_COLOR: Hsla = Hsla { h: 142. / 360., s: 0.68, diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs new file mode 100644 index 0000000000..7c369c9337 --- /dev/null +++ b/crates/project/src/git.rs @@ -0,0 +1,124 @@ +use std::sync::Arc; + +use futures::channel::mpsc; +use futures::StreamExt as _; +use git::repository::{GitRepository, RepoPath}; +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, + + /// 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)>, +} + +impl GitState { + pub fn new(cx: &AppContext) -> Self { + let (tx, mut rx) = + mpsc::unbounded::<(Arc, Vec, StatusAction)>(); + cx.spawn(|cx| async move { + while let Some((git_repo, paths, action)) = rx.next().await { + cx.background_executor() + .spawn(async move { + match action { + StatusAction::Stage => git_repo.stage_paths(&paths), + StatusAction::Unstage => git_repo.unstage_paths(&paths), + } + }) + .await + .log_err(); + } + }) + .detach(); + GitState { + commit_message: None, + active_repository: None, + update_sender: tx, + } + } + + pub fn activate_repository( + &mut self, + worktree_id: WorktreeId, + active_repository: RepositoryEntry, + git_repo: Arc, + ) { + self.active_repository = Some((worktree_id, active_repository, git_repo)); + } + + pub fn active_repository( + &self, + ) -> Option<&(WorktreeId, RepositoryEntry, Arc)> { + 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; + } + + pub fn stage_entry(&mut self, repo_path: RepoPath) { + if let Some((_, _, git_repo)) = self.active_repository.as_ref() { + let _ = self.update_sender.unbounded_send(( + git_repo.clone(), + vec![repo_path], + StatusAction::Stage, + )); + } + } + + pub fn unstage_entry(&mut self, repo_path: RepoPath) { + if let Some((_, _, git_repo)) = self.active_repository.as_ref() { + let _ = self.update_sender.unbounded_send(( + git_repo.clone(), + vec![repo_path], + StatusAction::Unstage, + )); + } + } + + pub fn stage_entries(&mut self, entries: Vec) { + if let Some((_, _, git_repo)) = self.active_repository.as_ref() { + let _ = + self.update_sender + .unbounded_send((git_repo.clone(), entries, StatusAction::Stage)); + } + } + + fn act_on_all(&mut self, action: StatusAction) { + if let Some((_, active_repository, git_repo)) = self.active_repository.as_ref() { + let _ = self.update_sender.unbounded_send(( + git_repo.clone(), + active_repository + .status() + .map(|entry| entry.repo_path) + .collect(), + action, + )); + } + } + + pub fn stage_all(&mut self) { + self.act_on_all(StatusAction::Stage); + } + + pub fn unstage_all(&mut self) { + self.act_on_all(StatusAction::Unstage); + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c67aeef9ec..3ffb861171 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2,6 +2,7 @@ pub mod buffer_store; mod color_extractor; pub mod connection_manager; pub mod debounced_delay; +pub mod git; pub mod image_store; pub mod lsp_command; pub mod lsp_ext_command; @@ -24,6 +25,7 @@ pub use environment::EnvironmentErrorMessage; pub mod search_history; mod yarn; +use crate::git::GitState; use anyhow::{anyhow, Context as _, Result}; use buffer_store::{BufferChangeSet, BufferStore, BufferStoreEvent}; use client::{proto, Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore}; @@ -39,7 +41,11 @@ use futures::{ pub use image_store::{ImageItem, ImageStore}; use image_store::{ImageItemEvent, ImageStoreEvent}; -use git::{blame::Blame, repository::GitRepository, status::FileStatus}; +use ::git::{ + blame::Blame, + repository::{Branch, GitRepository}, + status::FileStatus, +}; use gpui::{ AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla, Model, ModelContext, SharedString, Task, WeakModel, WindowContext, @@ -148,6 +154,7 @@ pub struct Project { fs: Arc, ssh_client: Option>, client_state: ProjectClientState, + git_state: Option>, collaborators: HashMap, client_subscriptions: Vec, worktree_store: Model, @@ -685,6 +692,9 @@ impl Project { cx, ) }); + + let git_state = Some(cx.new_model(|cx| GitState::new(cx))); + cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); Self { @@ -696,6 +706,7 @@ impl Project { lsp_store, join_project_response_message_id: 0, client_state: ProjectClientState::Local, + git_state, client_subscriptions: Vec::new(), _subscriptions: vec![cx.on_release(Self::release)], active_entry: None, @@ -814,6 +825,7 @@ impl Project { lsp_store, join_project_response_message_id: 0, client_state: ProjectClientState::Local, + git_state: None, client_subscriptions: Vec::new(), _subscriptions: vec![ cx.on_release(Self::release), @@ -1045,6 +1057,7 @@ impl Project { remote_id, replica_id, }, + git_state: None, buffers_needing_diff: Default::default(), git_diff_debouncer: DebouncedDelay::new(), terminals: Terminals { @@ -3534,7 +3547,7 @@ impl Project { &self, project_path: ProjectPath, cx: &AppContext, - ) -> Task>> { + ) -> Task>> { self.worktree_store().read(cx).branches(project_path, cx) } @@ -4154,6 +4167,10 @@ impl Project { pub fn buffer_store(&self) -> &Model { &self.buffer_store } + + pub fn git_state(&self) -> Option<&Model> { + self.git_state.as_ref() + } } fn deserialize_code_actions(code_actions: &HashMap) -> Vec { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index b216d15b1e..48c48c86e5 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,7 +1,7 @@ use crate::{Event, *}; +use ::git::diff::assert_hunks; use fs::FakeFs; use futures::{future, StreamExt}; -use git::diff::assert_hunks; use gpui::{AppContext, SemanticVersion, UpdateGlobal}; use http_client::Url; use language::{ diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a6b6f014ac..4c65b41da4 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2583,6 +2583,17 @@ impl Snapshot { &self.repositories } + pub fn repositories_with_abs_paths( + &self, + ) -> impl '_ + Iterator { + let base = self.abs_path(); + self.repositories.iter().map(|repo| { + let path = repo.work_directory.location_in_repo.as_deref(); + let path = path.unwrap_or(repo.work_directory.as_ref()); + (repo, base.join(path)) + }) + } + /// Get the repository whose work directory corresponds to the given path. pub(crate) fn repository(&self, work_directory: PathKey) -> Option { self.repositories.get(&work_directory, &()).cloned()