mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-24 02:46:43 +00:00
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:
parent
614eaec278
commit
b7726238ad
10 changed files with 363 additions and 317 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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();
|
||||
}
|
||||
}
|
||||
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<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,
|
||||
|
|
124
crates/project/src/git.rs
Normal file
124
crates/project/src/git.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue