git: Move all state into the panel (#23185)

This should fix the problem with the panel not updating when switching
projects.

Release Notes:

- N/A
This commit is contained in:
Cole Miller 2025-01-15 14:41:21 -05:00 committed by GitHub
parent d578f5ac37
commit e265e69429
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 81 additions and 121 deletions

View file

@ -88,7 +88,7 @@ pub struct GitPanel {
selected_entry: Option<usize>, selected_entry: Option<usize>,
show_scrollbar: bool, show_scrollbar: bool,
rebuild_requested: Arc<AtomicBool>, rebuild_requested: Arc<AtomicBool>,
git_state: Model<GitState>, git_state: GitState,
commit_editor: View<Editor>, commit_editor: View<Editor>,
/// The visible entries in the list, accounting for folding & expanded state. /// The visible entries in the list, accounting for folding & expanded state.
/// ///
@ -112,11 +112,8 @@ impl GitPanel {
let fs = workspace.app_state().fs.clone(); let fs = workspace.app_state().fs.clone();
let project = workspace.project().clone(); let project = workspace.project().clone();
let language_registry = workspace.app_state().languages.clone(); let language_registry = workspace.app_state().languages.clone();
let git_state = GitState::get_global(cx); let mut git_state = GitState::new(cx);
let current_commit_message = { let current_commit_message = git_state.commit_message.clone();
let state = git_state.read(cx);
state.commit_message.clone()
};
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| { let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
@ -128,89 +125,74 @@ impl GitPanel {
cx.subscribe(&project, move |this, project, event, cx| { cx.subscribe(&project, move |this, project, event, cx| {
use project::Event; use project::Event;
let git_state = &mut this.git_state;
let first_worktree_id = project.read(cx).worktrees(cx).next().map(|worktree| { let first_worktree_id = project.read(cx).worktrees(cx).next().map(|worktree| {
let snapshot = worktree.read(cx).snapshot(); let snapshot = worktree.read(cx).snapshot();
snapshot.id() snapshot.id()
}); });
let first_repo_in_project = first_repository_in_project(&project, cx); let first_repo_in_project = first_repository_in_project(&project, cx);
// TODO: Don't get another git_state here
// was running into a borrow issue
let git_state = GitState::get_global(cx);
match event { match event {
project::Event::WorktreeRemoved(id) => { project::Event::WorktreeRemoved(id) => {
git_state.update(cx, |state, _| { git_state.all_repositories.remove(id);
state.all_repositories.remove(id); let Some((worktree_id, _, _)) = git_state.active_repository.as_ref() else {
let Some((worktree_id, _, _)) = state.active_repository.as_ref() else { return;
return; };
}; if worktree_id == id {
if worktree_id == id { git_state.active_repository = first_repo_in_project;
state.active_repository = first_repo_in_project; this.schedule_update();
this.schedule_update(); }
}
});
} }
project::Event::WorktreeOrderChanged => { project::Event::WorktreeOrderChanged => {
// activate the new first worktree if the first was moved // activate the new first worktree if the first was moved
let Some(first_id) = first_worktree_id else { let Some(first_id) = first_worktree_id else {
return; return;
}; };
git_state.update(cx, |state, _| { if !git_state
if !state .active_repository
.active_repository .as_ref()
.as_ref() .is_some_and(|(id, _, _)| id == &first_id)
.is_some_and(|(id, _, _)| id == &first_id) {
{ git_state.active_repository = first_repo_in_project;
state.active_repository = first_repo_in_project; this.schedule_update();
this.schedule_update(); }
}
});
} }
Event::WorktreeAdded(id) => { Event::WorktreeAdded(id) => {
git_state.update(cx, |state, cx| { let Some(worktree) = project.read(cx).worktree_for_id(*id, cx) else {
let Some(worktree) = project.read(cx).worktree_for_id(*id, cx) else { return;
return; };
}; let snapshot = worktree.read(cx).snapshot();
let snapshot = worktree.read(cx).snapshot(); git_state
state .all_repositories
.all_repositories .insert(*id, snapshot.repositories().clone());
.insert(*id, snapshot.repositories().clone());
});
let Some(first_id) = first_worktree_id else { let Some(first_id) = first_worktree_id else {
return; return;
}; };
git_state.update(cx, |state, _| { if !git_state
if !state .active_repository
.active_repository .as_ref()
.as_ref() .is_some_and(|(id, _, _)| id == &first_id)
.is_some_and(|(id, _, _)| id == &first_id) {
{ git_state.active_repository = first_repo_in_project;
state.active_repository = first_repo_in_project; this.schedule_update();
this.schedule_update(); }
}
});
} }
project::Event::WorktreeUpdatedEntries(id, _) => { project::Event::WorktreeUpdatedEntries(id, _) => {
git_state.update(cx, |state, _| { if git_state
if state .active_repository
.active_repository .as_ref()
.as_ref() .is_some_and(|(active_id, _, _)| active_id == id)
.is_some_and(|(active_id, _, _)| active_id == id) {
{ git_state.active_repository = first_repo_in_project;
state.active_repository = first_repo_in_project; this.schedule_update();
this.schedule_update(); }
}
});
} }
project::Event::WorktreeUpdatedGitRepositories(_) => { project::Event::WorktreeUpdatedGitRepositories(_) => {
let Some(first) = first_repo_in_project else { let Some(first) = first_repo_in_project else {
return; return;
}; };
git_state.update(cx, |state, _| { git_state.active_repository = Some(first);
state.active_repository = Some(first); this.schedule_update();
this.schedule_update();
});
} }
project::Event::Closed => { project::Event::Closed => {
this.reveal_in_editor = Task::ready(()); this.reveal_in_editor = Task::ready(());
@ -272,20 +254,18 @@ impl GitPanel {
let scroll_handle = UniformListScrollHandle::new(); let scroll_handle = UniformListScrollHandle::new();
git_state.update(cx, |state, cx| { let mut visible_worktrees = project.read(cx).visible_worktrees(cx);
let mut visible_worktrees = project.read(cx).visible_worktrees(cx); let first_worktree = visible_worktrees.next();
let Some(first_worktree) = visible_worktrees.next() else { drop(visible_worktrees);
return; if let Some(first_worktree) = first_worktree {
};
drop(visible_worktrees);
let snapshot = first_worktree.read(cx).snapshot(); let snapshot = first_worktree.read(cx).snapshot();
if let Some((repo, git_repo)) = if let Some((repo, git_repo)) =
first_worktree_repository(&project, snapshot.id(), cx) first_worktree_repository(&project, snapshot.id(), cx)
{ {
state.activate_repository(snapshot.id(), repo, git_repo); git_state.activate_repository(snapshot.id(), repo, git_repo);
} }
}); };
let rebuild_requested = Arc::new(AtomicBool::new(false)); let rebuild_requested = Arc::new(AtomicBool::new(false));
let flag = rebuild_requested.clone(); let flag = rebuild_requested.clone();
@ -569,18 +549,16 @@ impl GitPanel {
.and_then(|i| self.visible_entries.get(i)) .and_then(|i| self.visible_entries.get(i))
} }
fn toggle_staged_for_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) { fn toggle_staged_for_entry(&mut self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
self.git_state match entry.status.is_staged() {
.clone() Some(true) | None => self.git_state.unstage_entry(entry.repo_path.clone()),
.update(cx, |state, _| match entry.status.is_staged() { Some(false) => self.git_state.stage_entry(entry.repo_path.clone()),
Some(true) | None => state.unstage_entry(entry.repo_path.clone()), }
Some(false) => state.stage_entry(entry.repo_path.clone()),
});
cx.notify(); cx.notify();
} }
fn toggle_staged_for_selected(&mut self, _: &ToggleStaged, cx: &mut ViewContext<Self>) { fn toggle_staged_for_selected(&mut self, _: &ToggleStaged, cx: &mut ViewContext<Self>) {
if let Some(selected_entry) = self.get_selected_entry() { if let Some(selected_entry) = self.get_selected_entry().cloned() {
self.toggle_staged_for_entry(&selected_entry, cx); self.toggle_staged_for_entry(&selected_entry, cx);
} }
} }
@ -595,11 +573,14 @@ impl GitPanel {
} }
fn open_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) { fn open_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
let Some((worktree_id, path)) = GitState::get_global(cx).update(cx, |state, _| { let Some((worktree_id, path)) =
state.active_repository.as_ref().and_then(|(id, repo, _)| { self.git_state
Some((*id, repo.work_directory.unrelativize(&entry.repo_path)?)) .active_repository
}) .as_ref()
}) else { .and_then(|(id, repo, _)| {
Some((*id, repo.work_directory.unrelativize(&entry.repo_path)?))
})
else {
return; return;
}; };
let path = (worktree_id, path).into(); let path = (worktree_id, path).into();
@ -612,7 +593,7 @@ impl GitPanel {
cx.emit(Event::OpenedEntry { path }); cx.emit(Event::OpenedEntry { path });
} }
fn stage_all(&mut self, _: &StageAll, cx: &mut ViewContext<Self>) { fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
let to_stage = self let to_stage = self
.visible_entries .visible_entries
.iter_mut() .iter_mut()
@ -623,19 +604,16 @@ impl GitPanel {
}) })
.collect(); .collect();
self.all_staged = Some(true); self.all_staged = Some(true);
self.git_state self.git_state.stage_entries(to_stage);
.update(cx, |state, _| state.stage_entries(to_stage));
} }
fn unstage_all(&mut self, _: &UnstageAll, cx: &mut ViewContext<Self>) { fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
// This should only be called when all entries are staged. // This should only be called when all entries are staged.
for entry in &mut self.visible_entries { for entry in &mut self.visible_entries {
entry.is_staged = Some(false); entry.is_staged = Some(false);
} }
self.all_staged = Some(false); self.all_staged = Some(false);
self.git_state.update(cx, |state, _| { self.git_state.unstage_all();
state.unstage_all();
});
} }
fn discard_all(&mut self, _: &RevertAll, _cx: &mut ViewContext<Self>) { fn discard_all(&mut self, _: &RevertAll, _cx: &mut ViewContext<Self>) {
@ -644,8 +622,7 @@ impl GitPanel {
} }
fn clear_message(&mut self, cx: &mut ViewContext<Self>) { fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
self.git_state self.git_state.clear_commit_message();
.update(cx, |state, _cx| state.clear_commit_message());
self.commit_editor self.commit_editor
.update(cx, |editor, cx| editor.set_text("", cx)); .update(cx, |editor, cx| editor.set_text("", cx));
} }
@ -713,11 +690,9 @@ impl GitPanel {
#[track_caller] #[track_caller]
fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) { fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
let git_state = self.git_state.read(cx);
self.visible_entries.clear(); self.visible_entries.clear();
let Some((_, repo, _)) = git_state.active_repository().as_ref() else { let Some((_, repo, _)) = self.git_state.active_repository().as_ref() else {
// Just clear entries if no repository is active. // Just clear entries if no repository is active.
cx.notify(); cx.notify();
return; return;
@ -790,9 +765,7 @@ impl GitPanel {
if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event { if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
let commit_message = self.commit_editor.update(cx, |editor, cx| editor.text(cx)); let commit_message = self.commit_editor.update(cx, |editor, cx| editor.text(cx));
self.git_state.update(cx, |state, _cx| { self.git_state.commit_message = Some(commit_message.into());
state.commit_message = Some(commit_message.into());
});
cx.notify(); cx.notify();
} }
@ -1096,7 +1069,6 @@ impl GitPanel {
entry_details: GitListEntry, entry_details: GitListEntry,
cx: &ViewContext<Self>, cx: &ViewContext<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let state = self.git_state.clone();
let repo_path = entry_details.repo_path.clone(); let repo_path = entry_details.repo_path.clone();
let selected = self.selected_entry == Some(ix); let selected = self.selected_entry == Some(ix);
let status_style = GitPanelSettings::get_global(cx).status_style; let status_style = GitPanelSettings::get_global(cx).status_style;
@ -1122,7 +1094,7 @@ impl GitPanel {
let entry_id = ElementId::Name(format!("entry_{}", entry_details.display_name).into()); let entry_id = ElementId::Name(format!("entry_{}", entry_details.display_name).into());
let checkbox_id = let checkbox_id =
ElementId::Name(format!("checkbox_{}", entry_details.display_name).into()); ElementId::Name(format!("checkbox_{}", entry_details.display_name).into());
let view_mode = state.read(cx).list_view_mode.clone(); let view_mode = self.git_state.list_view_mode;
let handle = cx.view().downgrade(); let handle = cx.view().downgrade();
let end_slot = h_flex() let end_slot = h_flex()
@ -1185,15 +1157,13 @@ impl GitPanel {
ToggleState::Selected => Some(true), ToggleState::Selected => Some(true),
ToggleState::Unselected => Some(false), ToggleState::Unselected => Some(false),
ToggleState::Indeterminate => None, ToggleState::Indeterminate => None,
} };
});
state.update(cx, {
let repo_path = repo_path.clone(); let repo_path = repo_path.clone();
move |state, _| match toggle { match toggle {
ToggleState::Selected | ToggleState::Indeterminate => { ToggleState::Selected | ToggleState::Indeterminate => {
state.stage_entry(repo_path); this.git_state.stage_entry(repo_path);
} }
ToggleState::Unselected => state.unstage_entry(repo_path), ToggleState::Unselected => this.git_state.unstage_entry(repo_path),
} }
}); });
} }

View file

@ -4,7 +4,7 @@ use futures::channel::mpsc;
use futures::StreamExt as _; use futures::StreamExt as _;
use git::repository::{GitFileStatus, GitRepository, RepoPath}; use git::repository::{GitFileStatus, GitRepository, RepoPath};
use git_panel_settings::GitPanelSettings; use git_panel_settings::GitPanelSettings;
use gpui::{actions, AppContext, Context, Global, Hsla, Model, ModelContext}; use gpui::{actions, AppContext, Hsla, Model};
use project::{Project, WorktreeId}; use project::{Project, WorktreeId};
use std::sync::Arc; use std::sync::Arc;
use sum_tree::SumTree; use sum_tree::SumTree;
@ -35,21 +35,15 @@ actions!(
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx); GitPanelSettings::register(cx);
let git_state = cx.new_model(GitState::new);
cx.set_global(GlobalGitState(git_state));
} }
#[derive(Default, Debug, PartialEq, Eq, Clone)] #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
pub enum GitViewMode { pub enum GitViewMode {
#[default] #[default]
List, List,
Tree, Tree,
} }
struct GlobalGitState(Model<GitState>);
impl Global for GlobalGitState {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StatusAction { enum StatusAction {
Stage, Stage,
@ -72,10 +66,10 @@ pub struct GitState {
} }
impl GitState { impl GitState {
pub fn new(cx: &mut ModelContext<'_, Self>) -> Self { pub fn new(cx: &AppContext) -> Self {
let (updater_tx, mut updater_rx) = let (updater_tx, mut updater_rx) =
mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>(); mpsc::unbounded::<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>();
cx.spawn(|_, cx| async move { cx.spawn(|cx| async move {
while let Some((git_repo, paths, action)) = updater_rx.next().await { while let Some((git_repo, paths, action)) = updater_rx.next().await {
cx.background_executor() cx.background_executor()
.spawn(async move { .spawn(async move {
@ -98,10 +92,6 @@ impl GitState {
} }
} }
pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
cx.global::<GlobalGitState>().0.clone()
}
pub fn activate_repository( pub fn activate_repository(
&mut self, &mut self,
worktree_id: WorktreeId, worktree_id: WorktreeId,