Detailed changes
@@ -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",
@@ -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!(
@@ -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);
@@ -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
@@ -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<usize>,
show_scrollbar: bool,
rebuild_requested: Arc<AtomicBool>,
- git_state: GitState,
commit_editor: View<Editor>,
/// 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<Project>,
+ worktree_id: WorktreeId,
+ cx: &mut AppContext,
+) -> Option<(RepositoryEntry, Arc<dyn GitRepository>)> {
+ 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<Project>,
+ cx: &mut AppContext,
+) -> Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
+ 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<Workspace>,
@@ -110,9 +147,11 @@ 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 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<Self>| {
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();
+ 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();
+ 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();
+ 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;
+ 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::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?
- }
- _ => {}
- };
+ 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<GitState>> {
+ self.project.read(cx).git_state()
+ }
+
+ fn active_repository<'a>(
+ &self,
+ cx: &'a AppContext,
+ ) -> Option<&'a (WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
+ 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<Self>) {
// 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<Self>) {
- 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<Self>) {
+ fn toggle_staged_for_selected(&mut self, _: &git::ToggleStaged, cx: &mut ViewContext<Self>) {
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<Self>) {
- 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<Self>) {
+ fn stage_all(&mut self, _: &git::StageAll, cx: &mut ViewContext<Self>) {
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<Self>) {
+ fn unstage_all(&mut self, _: &git::UnstageAll, cx: &mut ViewContext<Self>) {
// 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<Self>) {
+ fn discard_all(&mut self, _: &git::RevertAll, _cx: &mut ViewContext<Self>) {
// TODO: Implement discard all
println!("Discard all triggered");
}
fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
- 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<Self>) {
+ fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext<Self>) {
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<Self>) {
+ fn commit_all_changes(&mut self, _: &git::CommitAllChanges, cx: &mut ViewContext<Self>) {
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>) {
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),
+ })
});
}
}),
@@ -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<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.
- active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
-
- updater_tx: mpsc::UnboundedSender<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>,
-
- all_repositories: HashMap<WorktreeId, SumTree<RepositoryEntry>>,
-
- list_view_mode: GitViewMode,
-}
-
-impl GitState {
- pub fn new(cx: &AppContext) -> Self {
- let (updater_tx, mut updater_rx) =
- mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, 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<dyn GitRepository>,
- ) {
- self.active_repository = Some((worktree_id, active_repository, git_repo));
- }
-
- pub fn active_repository(
- &self,
- ) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
- 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;
- }
-
- 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<RepoPath>) {
- 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<Project>,
- worktree_id: WorktreeId,
- cx: &mut AppContext,
-) -> Option<(RepositoryEntry, Arc<dyn GitRepository>)> {
- 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<Project>,
- cx: &mut AppContext,
-) -> Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
- 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,
@@ -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<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)>,
+}
+
+impl GitState {
+ pub fn new(cx: &AppContext) -> Self {
+ let (tx, mut rx) =
+ mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, 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<dyn GitRepository>,
+ ) {
+ self.active_repository = Some((worktree_id, active_repository, git_repo));
+ }
+
+ pub fn active_repository(
+ &self,
+ ) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
+ 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;
+ }
+
+ 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<RepoPath>) {
+ 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);
+ }
+}
@@ -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<dyn Fs>,
ssh_client: Option<Model<SshRemoteClient>>,
client_state: ProjectClientState,
+ git_state: Option<Model<GitState>>,
collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
worktree_store: Model<WorktreeStore>,
@@ -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<Result<Vec<git::repository::Branch>>> {
+ ) -> Task<Result<Vec<Branch>>> {
self.worktree_store().read(cx).branches(project_path, cx)
}
@@ -4154,6 +4167,10 @@ impl Project {
pub fn buffer_store(&self) -> &Model<BufferStore> {
&self.buffer_store
}
+
+ pub fn git_state(&self) -> Option<&Model<GitState>> {
+ self.git_state.as_ref()
+ }
}
fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::CodeActionKind> {
@@ -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::{
@@ -2583,6 +2583,17 @@ impl Snapshot {
&self.repositories
}
+ pub fn repositories_with_abs_paths(
+ &self,
+ ) -> impl '_ + Iterator<Item = (&RepositoryEntry, PathBuf)> {
+ 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<RepositoryEntry> {
self.repositories.get(&work_directory, &()).cloned()