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>,
show_scrollbar: bool,
rebuild_requested: Arc<AtomicBool>,
git_state: Model<GitState>,
git_state: GitState,
commit_editor: View<Editor>,
/// 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 project = workspace.project().clone();
let language_registry = workspace.app_state().languages.clone();
let git_state = GitState::get_global(cx);
let current_commit_message = {
let state = git_state.read(cx);
state.commit_message.clone()
};
let mut git_state = GitState::new(cx);
let current_commit_message = git_state.commit_message.clone();
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
let focus_handle = cx.focus_handle();
@ -128,89 +125,74 @@ 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);
// TODO: Don't get another git_state here
// was running into a borrow issue
let git_state = GitState::get_global(cx);
match event {
project::Event::WorktreeRemoved(id) => {
git_state.update(cx, |state, _| {
state.all_repositories.remove(id);
let Some((worktree_id, _, _)) = state.active_repository.as_ref() else {
return;
};
if worktree_id == id {
state.active_repository = first_repo_in_project;
this.schedule_update();
}
});
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;
};
git_state.update(cx, |state, _| {
if !state
.active_repository
.as_ref()
.is_some_and(|(id, _, _)| id == &first_id)
{
state.active_repository = first_repo_in_project;
this.schedule_update();
}
});
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) => {
git_state.update(cx, |state, cx| {
let Some(worktree) = project.read(cx).worktree_for_id(*id, cx) else {
return;
};
let snapshot = worktree.read(cx).snapshot();
state
.all_repositories
.insert(*id, snapshot.repositories().clone());
});
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;
};
git_state.update(cx, |state, _| {
if !state
.active_repository
.as_ref()
.is_some_and(|(id, _, _)| id == &first_id)
{
state.active_repository = first_repo_in_project;
this.schedule_update();
}
});
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, _) => {
git_state.update(cx, |state, _| {
if state
.active_repository
.as_ref()
.is_some_and(|(active_id, _, _)| active_id == id)
{
state.active_repository = first_repo_in_project;
this.schedule_update();
}
});
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.update(cx, |state, _| {
state.active_repository = Some(first);
this.schedule_update();
});
git_state.active_repository = Some(first);
this.schedule_update();
}
project::Event::Closed => {
this.reveal_in_editor = Task::ready(());
@ -272,20 +254,18 @@ impl GitPanel {
let scroll_handle = UniformListScrollHandle::new();
git_state.update(cx, |state, cx| {
let mut visible_worktrees = project.read(cx).visible_worktrees(cx);
let Some(first_worktree) = visible_worktrees.next() else {
return;
};
drop(visible_worktrees);
let mut visible_worktrees = project.read(cx).visible_worktrees(cx);
let first_worktree = visible_worktrees.next();
drop(visible_worktrees);
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)
{
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 flag = rebuild_requested.clone();
@ -569,18 +549,16 @@ impl GitPanel {
.and_then(|i| self.visible_entries.get(i))
}
fn toggle_staged_for_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
self.git_state
.clone()
.update(cx, |state, _| match entry.status.is_staged() {
Some(true) | None => state.unstage_entry(entry.repo_path.clone()),
Some(false) => state.stage_entry(entry.repo_path.clone()),
});
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()),
}
cx.notify();
}
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);
}
}
@ -595,11 +573,14 @@ impl GitPanel {
}
fn open_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
let Some((worktree_id, path)) = GitState::get_global(cx).update(cx, |state, _| {
state.active_repository.as_ref().and_then(|(id, repo, _)| {
Some((*id, repo.work_directory.unrelativize(&entry.repo_path)?))
})
}) else {
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 {
return;
};
let path = (worktree_id, path).into();
@ -612,7 +593,7 @@ impl GitPanel {
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
.visible_entries
.iter_mut()
@ -623,19 +604,16 @@ impl GitPanel {
})
.collect();
self.all_staged = Some(true);
self.git_state
.update(cx, |state, _| state.stage_entries(to_stage));
self.git_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.
for entry in &mut self.visible_entries {
entry.is_staged = Some(false);
}
self.all_staged = Some(false);
self.git_state.update(cx, |state, _| {
state.unstage_all();
});
self.git_state.unstage_all();
}
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>) {
self.git_state
.update(cx, |state, _cx| state.clear_commit_message());
self.git_state.clear_commit_message();
self.commit_editor
.update(cx, |editor, cx| editor.set_text("", cx));
}
@ -713,11 +690,9 @@ impl GitPanel {
#[track_caller]
fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
let git_state = self.git_state.read(cx);
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.
cx.notify();
return;
@ -790,9 +765,7 @@ 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.update(cx, |state, _cx| {
state.commit_message = Some(commit_message.into());
});
self.git_state.commit_message = Some(commit_message.into());
cx.notify();
}
@ -1096,7 +1069,6 @@ impl GitPanel {
entry_details: GitListEntry,
cx: &ViewContext<Self>,
) -> impl IntoElement {
let state = self.git_state.clone();
let repo_path = entry_details.repo_path.clone();
let selected = self.selected_entry == Some(ix);
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 checkbox_id =
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 end_slot = h_flex()
@ -1185,15 +1157,13 @@ impl GitPanel {
ToggleState::Selected => Some(true),
ToggleState::Unselected => Some(false),
ToggleState::Indeterminate => None,
}
});
state.update(cx, {
};
let repo_path = repo_path.clone();
move |state, _| match toggle {
match toggle {
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 git::repository::{GitFileStatus, GitRepository, RepoPath};
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 std::sync::Arc;
use sum_tree::SumTree;
@ -35,21 +35,15 @@ actions!(
pub fn init(cx: &mut AppContext) {
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 {
#[default]
List,
Tree,
}
struct GlobalGitState(Model<GitState>);
impl Global for GlobalGitState {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StatusAction {
Stage,
@ -72,10 +66,10 @@ pub struct GitState {
}
impl GitState {
pub fn new(cx: &mut ModelContext<'_, Self>) -> Self {
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 {
cx.spawn(|cx| async move {
while let Some((git_repo, paths, action)) = updater_rx.next().await {
cx.background_executor()
.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(
&mut self,
worktree_id: WorktreeId,