Move git state to Project (#23208)

This restores its visibility outside of git_ui, which we'll need soon,
while preserving its per-project character.

Release Notes:

- N/A
This commit is contained in:
Cole Miller 2025-01-16 13:57:28 -05:00 committed by GitHub
parent 614eaec278
commit b7726238ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 363 additions and 317 deletions

2
Cargo.lock generated
View file

@ -5129,7 +5129,6 @@ dependencies = [
"collections", "collections",
"db", "db",
"editor", "editor",
"futures 0.3.31",
"git", "git",
"gpui", "gpui",
"language", "language",
@ -5140,7 +5139,6 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"settings", "settings",
"sum_tree",
"theme", "theme",
"ui", "ui",
"util", "util",

View file

@ -2942,7 +2942,7 @@ pub mod tests {
.update(cx, |editor, cx| { .update(cx, |editor, cx| {
assert_eq!( assert_eq!(
vec!["main hint #0".to_string(), "other hint #0".to_string()], 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" "Cache should update for both excerpts despite hints display was disabled"
); );
assert!( assert!(

View file

@ -7,6 +7,7 @@ pub mod repository;
pub mod status; pub mod status;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use gpui::actions;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt; use std::fmt;
@ -24,6 +25,24 @@ pub static FSMONITOR_DAEMON: LazyLock<&'static OsStr> =
LazyLock::new(|| OsStr::new("fsmonitor--daemon")); LazyLock::new(|| OsStr::new("fsmonitor--daemon"));
pub static GITIGNORE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".gitignore")); 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)] #[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Oid(libgit::Oid); pub struct Oid(libgit::Oid);

View file

@ -17,7 +17,6 @@ anyhow.workspace = true
collections.workspace = true collections.workspace = true
db.workspace = true db.workspace = true
editor.workspace = true editor.workspace = true
futures.workspace = true
git.workspace = true git.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true
@ -28,7 +27,6 @@ serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings.workspace = true settings.workspace = true
sum_tree.workspace = true
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true

View file

@ -1,18 +1,17 @@
use crate::git_panel_settings::StatusStyle; use crate::git_panel_settings::StatusStyle;
use crate::{first_repository_in_project, first_worktree_repository}; use crate::{git_panel_settings::GitPanelSettings, git_status_icon};
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, CommitAllChanges, CommitChanges,
GitState, GitViewMode, RevertAll, StageAll, ToggleStaged, UnstageAll,
};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::scroll::ScrollbarAutoHide; use editor::scroll::ScrollbarAutoHide;
use editor::{Editor, EditorSettings, ShowScrollbar}; 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 gpui::*;
use language::Buffer; use language::Buffer;
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; 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 serde::{Deserialize, Serialize};
use settings::Settings as _; use settings::Settings as _;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@ -21,12 +20,13 @@ use theme::ThemeSettings;
use ui::{ use ui::{
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip, prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
}; };
use util::{ResultExt, TryFutureExt}; use util::{maybe, ResultExt, TryFutureExt};
use workspace::notifications::DetachAndPromptErr; use workspace::notifications::DetachAndPromptErr;
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
Workspace, Workspace,
}; };
use worktree::RepositoryEntry;
actions!( actions!(
git_panel, git_panel,
@ -87,7 +87,6 @@ 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: 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.
/// ///
@ -99,6 +98,44 @@ pub struct GitPanel {
reveal_in_editor: Task<()>, 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 { impl GitPanel {
pub fn load( pub fn load(
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
@ -110,9 +147,11 @@ impl GitPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
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 git_state = project.read(cx).git_state().cloned();
let language_registry = workspace.app_state().languages.clone(); let language_registry = workspace.app_state().languages.clone();
let mut git_state = GitState::new(cx); let current_commit_message = git_state
let current_commit_message = git_state.commit_message.clone(); .as_ref()
.and_then(|git_state| git_state.read(cx).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();
@ -124,17 +163,20 @@ 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);
let Some(git_state) = project.read(cx).git_state().cloned() else {
return;
};
git_state.update(cx, |git_state, _| {
match event { match event {
project::Event::WorktreeRemoved(id) => { project::Event::WorktreeRemoved(id) => {
git_state.all_repositories.remove(id); let Some((worktree_id, _, _)) = git_state.active_repository.as_ref()
let Some((worktree_id, _, _)) = git_state.active_repository.as_ref() else { else {
return; return;
}; };
if worktree_id == id { if worktree_id == id {
@ -156,14 +198,7 @@ impl GitPanel {
this.schedule_update(); this.schedule_update();
} }
} }
Event::WorktreeAdded(id) => { Event::WorktreeAdded(_) => {
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 { let Some(first_id) = first_worktree_id else {
return; return;
}; };
@ -196,10 +231,10 @@ impl GitPanel {
project::Event::Closed => { project::Event::Closed => {
this.reveal_in_editor = Task::ready(()); this.reveal_in_editor = Task::ready(());
this.visible_entries.clear(); this.visible_entries.clear();
// TODO cancel/clear task?
} }
_ => {} _ => {}
}; };
});
}) })
.detach(); .detach();
@ -259,10 +294,12 @@ impl GitPanel {
if let Some(first_worktree) = first_worktree { if let Some(first_worktree) = first_worktree {
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), git_state)) =
first_worktree_repository(&project, snapshot.id(), cx) first_worktree_repository(&project, snapshot.id(), cx).zip(git_state)
{ {
git_state.update(cx, |git_state, _| {
git_state.activate_repository(snapshot.id(), repo, git_repo); git_state.activate_repository(snapshot.id(), repo, git_repo);
});
} }
}; };
@ -300,7 +337,6 @@ impl GitPanel {
hide_scrollbar_task: None, hide_scrollbar_task: None,
rebuild_requested, rebuild_requested,
commit_editor, commit_editor,
git_state,
reveal_in_editor: Task::ready(()), reveal_in_editor: Task::ready(()),
project, project,
}; };
@ -327,6 +363,19 @@ impl GitPanel {
git_panel 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>) { fn serialize(&mut self, cx: &mut ViewContext<Self>) {
// TODO: we can store stage status here // TODO: we can store stage status here
let width = self.width; let width = self.width;
@ -549,14 +598,20 @@ impl GitPanel {
} }
fn toggle_staged_for_entry(&mut self, entry: &GitListEntry, cx: &mut ViewContext<Self>) { fn toggle_staged_for_entry(&mut self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
match entry.status.is_staged() { let Some(git_state) = self.git_state(cx).cloned() else {
Some(true) | None => self.git_state.unstage_entry(entry.repo_path.clone()), return;
Some(false) => self.git_state.stage_entry(entry.repo_path.clone()), };
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(); 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() { 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);
} }
@ -572,14 +627,12 @@ 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)) = let Some((worktree_id, path)) = maybe!({
self.git_state let git_state = self.git_state(cx)?;
.active_repository let (id, repo, _) = git_state.read(cx).active_repository.as_ref()?;
.as_ref() let path = repo.work_directory.unrelativize(&entry.repo_path)?;
.and_then(|(id, repo, _)| { Some((*id, path))
Some((*id, repo.work_directory.unrelativize(&entry.repo_path)?)) }) else {
})
else {
return; return;
}; };
let path = (worktree_id, path).into(); let path = (worktree_id, path).into();
@ -592,7 +645,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, _: &git::StageAll, cx: &mut ViewContext<Self>) {
let to_stage = self let to_stage = self
.visible_entries .visible_entries
.iter_mut() .iter_mut()
@ -603,31 +656,42 @@ impl GitPanel {
}) })
.collect(); .collect();
self.all_staged = Some(true); 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. // 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.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 // TODO: Implement discard all
println!("Discard all triggered"); println!("Discard all triggered");
} }
fn clear_message(&mut self, cx: &mut ViewContext<Self>) { 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 self.commit_editor
.update(cx, |editor, cx| editor.set_text("", cx)); .update(cx, |editor, cx| editor.set_text("", cx));
} }
/// Commit all staged changes /// 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); self.clear_message(cx);
// TODO: Implement commit all staged // TODO: Implement commit all staged
@ -635,7 +699,7 @@ impl GitPanel {
} }
/// Commit all changes, regardless of whether they are staged or not /// 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); self.clear_message(cx);
// TODO: Implement commit all changes // TODO: Implement commit all changes
@ -691,7 +755,7 @@ impl GitPanel {
fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) { fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
self.visible_entries.clear(); 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. // Just clear entries if no repository is active.
cx.notify(); cx.notify();
return; return;
@ -764,7 +828,12 @@ 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.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(); cx.notify();
} }
@ -1094,7 +1163,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 = self.git_state.list_view_mode; let is_tree_view = false;
let handle = cx.view().downgrade(); let handle = cx.view().downgrade();
let end_slot = h_flex() let end_slot = h_flex()
@ -1125,7 +1194,7 @@ impl GitPanel {
this.hover(|this| this.bg(cx.theme().colors().ghost_element_hover)) 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)) entry = entry.pl(px(8. + 12. * entry_details.depth as f32))
} else { } else {
entry = entry.pl(px(8.)) entry = entry.pl(px(8.))
@ -1152,19 +1221,22 @@ impl GitPanel {
let Some(this) = handle.upgrade() else { let Some(this) = handle.upgrade() else {
return; return;
}; };
this.update(cx, |this, _| { this.update(cx, |this, cx| {
this.visible_entries[ix].is_staged = match *toggle { this.visible_entries[ix].is_staged = match *toggle {
ToggleState::Selected => Some(true), ToggleState::Selected => Some(true),
ToggleState::Unselected => Some(false), ToggleState::Unselected => Some(false),
ToggleState::Indeterminate => None, ToggleState::Indeterminate => None,
}; };
let repo_path = repo_path.clone(); 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 => { 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),
})
}); });
} }
}), }),

View file

@ -1,209 +1,16 @@
use ::settings::Settings; 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::status::FileStatus;
use git_panel_settings::GitPanelSettings; use git_panel_settings::GitPanelSettings;
use gpui::{actions, AppContext, Hsla, Model}; use gpui::{AppContext, Hsla};
use project::{Project, WorktreeId}; use ui::{Color, Icon, IconName, IntoElement};
use std::sync::Arc;
use sum_tree::SumTree;
use ui::{Color, Icon, IconName, IntoElement, SharedString};
use util::ResultExt as _;
use worktree::RepositoryEntry;
pub mod git_panel; pub mod git_panel;
mod git_panel_settings; 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) { pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx); 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 { const ADDED_COLOR: Hsla = Hsla {
h: 142. / 360., h: 142. / 360.,
s: 0.68, s: 0.68,

124
crates/project/src/git.rs Normal file
View file

@ -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);
}
}

View file

@ -2,6 +2,7 @@ pub mod buffer_store;
mod color_extractor; mod color_extractor;
pub mod connection_manager; pub mod connection_manager;
pub mod debounced_delay; pub mod debounced_delay;
pub mod git;
pub mod image_store; pub mod image_store;
pub mod lsp_command; pub mod lsp_command;
pub mod lsp_ext_command; pub mod lsp_ext_command;
@ -24,6 +25,7 @@ pub use environment::EnvironmentErrorMessage;
pub mod search_history; pub mod search_history;
mod yarn; mod yarn;
use crate::git::GitState;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use buffer_store::{BufferChangeSet, BufferStore, BufferStoreEvent}; use buffer_store::{BufferChangeSet, BufferStore, BufferStoreEvent};
use client::{proto, Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore}; use client::{proto, Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore};
@ -39,7 +41,11 @@ use futures::{
pub use image_store::{ImageItem, ImageStore}; pub use image_store::{ImageItem, ImageStore};
use image_store::{ImageItemEvent, ImageStoreEvent}; use image_store::{ImageItemEvent, ImageStoreEvent};
use git::{blame::Blame, repository::GitRepository, status::FileStatus}; use ::git::{
blame::Blame,
repository::{Branch, GitRepository},
status::FileStatus,
};
use gpui::{ use gpui::{
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla, AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
Model, ModelContext, SharedString, Task, WeakModel, WindowContext, Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
@ -148,6 +154,7 @@ pub struct Project {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
ssh_client: Option<Model<SshRemoteClient>>, ssh_client: Option<Model<SshRemoteClient>>,
client_state: ProjectClientState, client_state: ProjectClientState,
git_state: Option<Model<GitState>>,
collaborators: HashMap<proto::PeerId, Collaborator>, collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>, client_subscriptions: Vec<client::Subscription>,
worktree_store: Model<WorktreeStore>, worktree_store: Model<WorktreeStore>,
@ -685,6 +692,9 @@ impl Project {
cx, cx,
) )
}); });
let git_state = Some(cx.new_model(|cx| GitState::new(cx)));
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
Self { Self {
@ -696,6 +706,7 @@ impl Project {
lsp_store, lsp_store,
join_project_response_message_id: 0, join_project_response_message_id: 0,
client_state: ProjectClientState::Local, client_state: ProjectClientState::Local,
git_state,
client_subscriptions: Vec::new(), client_subscriptions: Vec::new(),
_subscriptions: vec![cx.on_release(Self::release)], _subscriptions: vec![cx.on_release(Self::release)],
active_entry: None, active_entry: None,
@ -814,6 +825,7 @@ impl Project {
lsp_store, lsp_store,
join_project_response_message_id: 0, join_project_response_message_id: 0,
client_state: ProjectClientState::Local, client_state: ProjectClientState::Local,
git_state: None,
client_subscriptions: Vec::new(), client_subscriptions: Vec::new(),
_subscriptions: vec![ _subscriptions: vec![
cx.on_release(Self::release), cx.on_release(Self::release),
@ -1045,6 +1057,7 @@ impl Project {
remote_id, remote_id,
replica_id, replica_id,
}, },
git_state: None,
buffers_needing_diff: Default::default(), buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(), git_diff_debouncer: DebouncedDelay::new(),
terminals: Terminals { terminals: Terminals {
@ -3534,7 +3547,7 @@ impl Project {
&self, &self,
project_path: ProjectPath, project_path: ProjectPath,
cx: &AppContext, cx: &AppContext,
) -> Task<Result<Vec<git::repository::Branch>>> { ) -> Task<Result<Vec<Branch>>> {
self.worktree_store().read(cx).branches(project_path, cx) self.worktree_store().read(cx).branches(project_path, cx)
} }
@ -4154,6 +4167,10 @@ impl Project {
pub fn buffer_store(&self) -> &Model<BufferStore> { pub fn buffer_store(&self) -> &Model<BufferStore> {
&self.buffer_store &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> { fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::CodeActionKind> {

View file

@ -1,7 +1,7 @@
use crate::{Event, *}; use crate::{Event, *};
use ::git::diff::assert_hunks;
use fs::FakeFs; use fs::FakeFs;
use futures::{future, StreamExt}; use futures::{future, StreamExt};
use git::diff::assert_hunks;
use gpui::{AppContext, SemanticVersion, UpdateGlobal}; use gpui::{AppContext, SemanticVersion, UpdateGlobal};
use http_client::Url; use http_client::Url;
use language::{ use language::{

View file

@ -2583,6 +2583,17 @@ impl Snapshot {
&self.repositories &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. /// Get the repository whose work directory corresponds to the given path.
pub(crate) fn repository(&self, work_directory: PathKey) -> Option<RepositoryEntry> { pub(crate) fn repository(&self, work_directory: PathKey) -> Option<RepositoryEntry> {
self.repositories.get(&work_directory, &()).cloned() self.repositories.get(&work_directory, &()).cloned()