From e3830d2ef54c618f8608f7954f352cc328fe694c Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Mon, 7 Apr 2025 11:10:01 -0700 Subject: [PATCH] Git activity indicator (#28204) Closes #26182 Release Notes: - Added an activity indicator for long-running git commands. --------- Co-authored-by: Mikayla Maki --- .../src/activity_indicator.rs | 48 +- crates/agent/src/message_editor.rs | 3 +- crates/agent/src/thread.rs | 22 +- crates/collab/src/tests/integration_tests.rs | 24 +- .../remote_editing_collaboration_tests.rs | 26 +- crates/editor/src/git/blame.rs | 4 +- crates/git_ui/src/branch_picker.rs | 23 +- crates/git_ui/src/git_panel.rs | 69 +- crates/project/src/git_store.rs | 855 ++++++++++-------- crates/project/src/project.rs | 14 +- .../remote_server/src/remote_editing_tests.rs | 24 +- 11 files changed, 625 insertions(+), 487 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 7fe4789591..2409865285 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -11,13 +11,22 @@ use language::{BinaryStatus, LanguageRegistry, LanguageServerId}; use project::{ EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project, ProjectEnvironmentEvent, + git_store::{GitStoreEvent, Repository}, }; use smallvec::SmallVec; -use std::{cmp::Reverse, fmt::Write, path::Path, sync::Arc, time::Duration}; +use std::{ + cmp::Reverse, + fmt::Write, + path::Path, + sync::Arc, + time::{Duration, Instant}, +}; use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; use util::truncate_and_trailoff; use workspace::{StatusItemView, Workspace, item::ItemHandle}; +const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0); + actions!(activity_indicator, [ShowErrorMessage]); pub enum Event { @@ -105,6 +114,15 @@ impl ActivityIndicator { ) .detach(); + cx.subscribe( + &project.read(cx).git_store().clone(), + |_, _, event: &GitStoreEvent, cx| match event { + project::git_store::GitStoreEvent::JobsUpdated => cx.notify(), + _ => {} + }, + ) + .detach(); + if let Some(auto_updater) = auto_updater.as_ref() { cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); } @@ -285,6 +303,34 @@ impl ActivityIndicator { }); } + let current_job = self + .project + .read(cx) + .active_repository(cx) + .map(|r| r.read(cx)) + .and_then(Repository::current_job); + // Show any long-running git command + if let Some(job_info) = current_job { + if Instant::now() - job_info.start >= GIT_OPERATION_DELAY { + return Some(Content { + icon: Some( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ) + .into_any_element(), + ), + message: job_info.message.into(), + on_click: None, + }); + } + } + // Show any language server installation info. let mut downloading = SmallVec::<[_; 3]>::new(); let mut checking_for_update = SmallVec::<[_; 3]>::new(); diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index 37f4c63d63..9f3324b3c5 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -226,7 +226,8 @@ impl MessageEditor { let thread = self.thread.clone(); let context_store = self.context_store.clone(); - let checkpoint = self.project.read(cx).git_store().read(cx).checkpoint(cx); + let git_store = self.project.read(cx).git_store().clone(); + let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx)); cx.spawn(async move |this, cx| { let checkpoint = checkpoint.await.ok(); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index cd88c5e483..6bdd92e785 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -471,11 +471,11 @@ impl Thread { cx.emit(ThreadEvent::CheckpointChanged); cx.notify(); - let project = self.project.read(cx); - let restore = project - .git_store() - .read(cx) - .restore_checkpoint(checkpoint.git_checkpoint.clone(), cx); + let git_store = self.project().read(cx).git_store().clone(); + let restore = git_store.update(cx, |git_store, cx| { + git_store.restore_checkpoint(checkpoint.git_checkpoint.clone(), cx) + }); + cx.spawn(async move |this, cx| { let result = restore.await; this.update(cx, |this, cx| { @@ -506,11 +506,11 @@ impl Thread { }; let git_store = self.project.read(cx).git_store().clone(); - let final_checkpoint = git_store.read(cx).checkpoint(cx); + let final_checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx)); cx.spawn(async move |this, cx| match final_checkpoint.await { Ok(final_checkpoint) => { let equal = git_store - .read_with(cx, |store, cx| { + .update(cx, |store, cx| { store.compare_checkpoints( pending_checkpoint.git_checkpoint.clone(), final_checkpoint.clone(), @@ -522,7 +522,7 @@ impl Thread { if equal { git_store - .read_with(cx, |store, cx| { + .update(cx, |store, cx| { store.delete_checkpoint(pending_checkpoint.git_checkpoint, cx) })? .detach(); @@ -533,7 +533,7 @@ impl Thread { } git_store - .read_with(cx, |store, cx| { + .update(cx, |store, cx| { store.delete_checkpoint(final_checkpoint, cx) })? .detach(); @@ -1650,10 +1650,10 @@ impl Thread { .ok() .flatten() .map(|repo| { - repo.read_with(cx, |repo, _| { + repo.update(cx, |repo, _| { let current_branch = repo.branch.as_ref().map(|branch| branch.name.to_string()); - repo.send_job(|state, _| async move { + repo.send_job(None, |state, _| async move { let RepositoryState::Local { backend, .. } = state else { return GitState { remote_url: None, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 1cc550b011..a965f2395a 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6866,10 +6866,14 @@ async fn test_remote_git_branches( assert_eq!(branches_b, branches_set); - cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string())) - .await - .unwrap() - .unwrap(); + cx_b.update(|cx| { + repo_b.update(cx, |repository, _cx| { + repository.change_branch(new_branch.to_string()) + }) + }) + .await + .unwrap() + .unwrap(); executor.run_until_parked(); @@ -6892,18 +6896,18 @@ async fn test_remote_git_branches( // Also try creating a new branch cx_b.update(|cx| { - repo_b - .read(cx) - .create_branch("totally-new-branch".to_string()) + repo_b.update(cx, |repository, _cx| { + repository.create_branch("totally-new-branch".to_string()) + }) }) .await .unwrap() .unwrap(); cx_b.update(|cx| { - repo_b - .read(cx) - .change_branch("totally-new-branch".to_string()) + repo_b.update(cx, |repository, _cx| { + repository.change_branch("totally-new-branch".to_string()) + }) }) .await .unwrap() diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index de7bb1c23b..41bf79fc6c 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -283,7 +283,7 @@ async fn test_ssh_collaboration_git_branches( let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap()); let branches_b = cx_b - .update(|cx| repo_b.read(cx).branches()) + .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches())) .await .unwrap() .unwrap(); @@ -297,10 +297,14 @@ async fn test_ssh_collaboration_git_branches( assert_eq!(&branches_b, &branches_set); - cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string())) - .await - .unwrap() - .unwrap(); + cx_b.update(|cx| { + repo_b.update(cx, |repo_b, _cx| { + repo_b.change_branch(new_branch.to_string()) + }) + }) + .await + .unwrap() + .unwrap(); executor.run_until_parked(); @@ -325,18 +329,18 @@ async fn test_ssh_collaboration_git_branches( // Also try creating a new branch cx_b.update(|cx| { - repo_b - .read(cx) - .create_branch("totally-new-branch".to_string()) + repo_b.update(cx, |repo_b, _cx| { + repo_b.create_branch("totally-new-branch".to_string()) + }) }) .await .unwrap() .unwrap(); cx_b.update(|cx| { - repo_b - .read(cx) - .change_branch("totally-new-branch".to_string()) + repo_b.update(cx, |repo_b, _cx| { + repo_b.change_branch("totally-new-branch".to_string()) + }) }) .await .unwrap() diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index a6485c4cf6..84b1e8d317 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -436,7 +436,9 @@ impl GitBlame { } let buffer_edits = self.buffer.update(cx, |buffer, _| buffer.subscribe()); let snapshot = self.buffer.read(cx).snapshot(); - let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx); + let blame = self.project.update(cx, |project, cx| { + project.blame_buffer(&self.buffer, None, cx) + }); let provider_registry = GitHostingProviderRegistry::default_global(cx); self.task = cx.spawn(async move |this, cx| { diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index c607916256..2c619cbd6f 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -88,7 +88,7 @@ impl BranchList { ) -> Self { let all_branches_request = repository .clone() - .map(|repository| repository.read(cx).branches()); + .map(|repository| repository.update(cx, |repository, _| repository.branches())); cx.spawn_in(window, async move |this, cx| { let mut all_branches = all_branches_request @@ -202,10 +202,15 @@ impl BranchListDelegate { return; }; cx.spawn(async move |_, cx| { - cx.update(|cx| repo.read(cx).create_branch(new_branch_name.to_string()))? - .await??; - cx.update(|cx| repo.read(cx).change_branch(new_branch_name.to_string()))? - .await??; + repo.update(cx, |repo, _| { + repo.create_branch(new_branch_name.to_string()) + })? + .await??; + repo.update(cx, |repo, _| { + repo.change_branch(new_branch_name.to_string()) + })? + .await??; + Ok(()) }) .detach_and_prompt_err("Failed to create branch", window, cx, |e, _, _| { @@ -359,11 +364,13 @@ impl PickerDelegate for BranchListDelegate { .ok_or_else(|| anyhow!("No active repository"))? .clone(); - let cx = cx.to_async(); + let mut cx = cx.to_async(); anyhow::Ok(async move { - cx.update(|cx| repo.read(cx).change_branch(branch.name.to_string()))? - .await? + repo.update(&mut cx, |repo, _| { + repo.change_branch(branch.name.to_string()) + })? + .await? }) })??; diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 7853784b57..8bc727cb19 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -53,10 +53,8 @@ use project::{ }; use serde::{Deserialize, Serialize}; use settings::{Settings as _, SettingsStore}; -use std::cell::RefCell; use std::future::Future; use std::path::{Path, PathBuf}; -use std::rc::Rc; use std::{collections::HashSet, sync::Arc, time::Duration, usize}; use strum::{IntoEnumIterator, VariantNames}; use time::OffsetDateTime; @@ -64,7 +62,7 @@ use ui::{ Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, Tooltip, prelude::*, }; -use util::{ResultExt, TryFutureExt, maybe, post_inc}; +use util::{ResultExt, TryFutureExt, maybe}; use workspace::AppState; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -232,8 +230,6 @@ struct PendingOperation { op_id: usize, } -type RemoteOperations = Rc>>; - // computed state related to how to render scrollbars // one per axis // on render we just read this off the panel @@ -290,8 +286,6 @@ impl ScrollbarProperties { } pub struct GitPanel { - remote_operation_id: u32, - pending_remote_operations: RemoteOperations, pub(crate) active_repository: Option>, pub(crate) commit_editor: Entity, conflicted_count: usize, @@ -327,17 +321,6 @@ pub struct GitPanel { _settings_subscription: Subscription, } -struct RemoteOperationGuard { - id: u32, - pending_remote_operations: RemoteOperations, -} - -impl Drop for RemoteOperationGuard { - fn drop(&mut self) { - self.pending_remote_operations.borrow_mut().remove(&self.id); - } -} - const MAX_PANEL_EDITOR_LINES: usize = 6; pub(crate) fn commit_message_editor( @@ -416,7 +399,7 @@ impl GitPanel { ) => { this.schedule_update(*full_scan, window, cx); } - GitStoreEvent::RepositoryUpdated(_, _, _) => {} + GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => { this.schedule_update(false, window, cx); } @@ -427,6 +410,8 @@ impl GitPanel { }) .ok(); } + GitStoreEvent::RepositoryUpdated(_, _, _) => {} + GitStoreEvent::JobsUpdated => {} }, ) .detach(); @@ -458,8 +443,6 @@ impl GitPanel { }); let mut git_panel = Self { - pending_remote_operations: Default::default(), - remote_operation_id: 0, active_repository, commit_editor, conflicted_count: 0, @@ -671,16 +654,6 @@ impl GitPanel { cx.notify(); } - fn start_remote_operation(&mut self) -> RemoteOperationGuard { - let id = post_inc(&mut self.remote_operation_id); - self.pending_remote_operations.borrow_mut().insert(id); - - RemoteOperationGuard { - id, - pending_remote_operations: self.pending_remote_operations.clone(), - } - } - fn serialize(&mut self, cx: &mut Context) { let width = self.width; self.pending_serialization = cx.background_spawn( @@ -1743,7 +1716,6 @@ impl GitPanel { return; }; telemetry::event!("Git Fetched"); - let guard = self.start_remote_operation(); let askpass = self.askpass_delegate("git fetch", window, cx); let this = cx.weak_entity(); window @@ -1751,7 +1723,6 @@ impl GitPanel { let fetch = repo.update(cx, |repo, cx| repo.fetch(askpass, cx))?; let remote_message = fetch.await?; - drop(guard); this.update(cx, |this, cx| { let action = RemoteAction::Fetch; match remote_message { @@ -1883,16 +1854,11 @@ impl GitPanel { this.askpass_delegate(format!("git pull {}", remote.name), window, cx) })?; - let guard = this - .update(cx, |this, _| this.start_remote_operation()) - .ok(); - let pull = repo.update(cx, |repo, cx| { repo.pull(branch.name.clone(), remote.name.clone(), askpass, cx) })?; let remote_message = pull.await?; - drop(guard); let action = RemoteAction::Pull(remote); this.update(cx, |this, cx| match remote_message { @@ -1954,10 +1920,6 @@ impl GitPanel { this.askpass_delegate(format!("git push {}", remote.name), window, cx) })?; - let guard = this - .update(cx, |this, _| this.start_remote_operation()) - .ok(); - let push = repo.update(cx, |repo, cx| { repo.push( branch.name.clone(), @@ -1969,7 +1931,6 @@ impl GitPanel { })?; let remote_output = push.await?; - drop(guard); let action = RemoteAction::Push(branch.name, remote); this.update(cx, |this, cx| match remote_output { @@ -2590,20 +2551,6 @@ impl GitPanel { workspace.add_item_to_center(Box::new(editor), window, cx); } - pub fn render_spinner(&self) -> Option { - (!self.pending_remote_operations.borrow().is_empty()).then(|| { - Icon::new(IconName::ArrowCircle) - .size(IconSize::XSmall) - .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) - .into_any_element() - }) - } - pub fn can_commit(&self) -> bool { (self.has_staged_changes() || self.has_tracked_changes()) && !self.has_unstaged_conflicts() } @@ -2832,12 +2779,10 @@ impl GitPanel { if !self.can_push_and_pull(cx) { return None; } - let spinner = self.render_spinner(); Some( h_flex() .gap_1() .flex_shrink_0() - .children(spinner) .when_some(branch, |this, branch| { let focus_handle = Some(self.focus_handle(cx)); @@ -4129,17 +4074,17 @@ impl RenderOnce for PanelRepoFooter { .truncate(true) .tooltip(Tooltip::for_action_title( "Switch Branch", - &zed_actions::git::Branch, + &zed_actions::git::Switch, )) .on_click(|_, window, cx| { - window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); + window.dispatch_action(zed_actions::git::Switch.boxed_clone(), cx); }); let branch_selector = PopoverMenu::new("popover-button") .menu(move |window, cx| Some(branch_picker::popover(repo.clone(), window, cx))) .trigger_with_tooltip( branch_selector_button, - Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch), + Tooltip::for_action_title("Switch Branch", &zed_actions::git::Switch), ) .anchor(Corner::BottomLeft) .offset(gpui::Point { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index a58cc699bf..f1aca42fbc 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -54,10 +54,11 @@ use std::{ Arc, atomic::{self, AtomicU64}, }, + time::Instant, }; use sum_tree::{Edit, SumTree, TreeSet}; use text::{Bias, BufferId}; -use util::{ResultExt, debug_panic}; +use util::{ResultExt, debug_panic, post_inc}; use worktree::{ File, PathKey, PathProgress, PathSummary, PathTarget, UpdatedGitRepositoriesSet, Worktree, }; @@ -224,7 +225,16 @@ pub struct RepositorySnapshot { pub scan_id: u64, } +type JobId = u64; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct JobInfo { + pub start: Instant, + pub message: SharedString, +} + pub struct Repository { + this: WeakEntity, snapshot: RepositorySnapshot, commit_message_buffer: Option>, git_store: WeakEntity, @@ -232,6 +242,8 @@ pub struct Repository { // and that should be examined during the next status scan. paths_needing_status_update: BTreeSet, job_sender: mpsc::UnboundedSender, + active_jobs: HashMap, + job_id: JobId, askpass_delegates: Arc>>, latest_askpass_id: u64, } @@ -262,6 +274,9 @@ pub enum RepositoryEvent { MergeHeadsChanged, } +#[derive(Clone, Debug)] +pub struct JobsUpdated; + #[derive(Debug)] pub enum GitStoreEvent { ActiveRepositoryChanged(Option), @@ -269,12 +284,14 @@ pub enum GitStoreEvent { RepositoryAdded(RepositoryId), RepositoryRemoved(RepositoryId), IndexWriteError(anyhow::Error), + JobsUpdated, } impl EventEmitter for Repository {} +impl EventEmitter for Repository {} impl EventEmitter for GitStore {} -struct GitJob { +pub struct GitJob { job: Box Task<()>>, key: Option, } @@ -552,7 +569,9 @@ impl GitStore { .loading_diffs .entry((buffer_id, DiffKind::Unstaged)) .or_insert_with(|| { - let staged_text = repo.read(cx).load_staged_text(buffer_id, repo_path, cx); + let staged_text = repo.update(cx, |repo, cx| { + repo.load_staged_text(buffer_id, repo_path, cx) + }); cx.spawn(async move |this, cx| { Self::open_diff_internal( this, @@ -607,7 +626,10 @@ impl GitStore { .loading_diffs .entry((buffer_id, DiffKind::Uncommitted)) .or_insert_with(|| { - let changes = repo.read(cx).load_committed_text(buffer_id, repo_path, cx); + let changes = repo.update(cx, |repo, cx| { + repo.load_committed_text(buffer_id, repo_path, cx) + }); + cx.spawn(async move |this, cx| { Self::open_diff_internal(this, DiffKind::Uncommitted, changes.await, buffer, cx) .await @@ -709,13 +731,14 @@ impl GitStore { Some(repo.read(cx).status_for_path(&repo_path)?.status) } - pub fn checkpoint(&self, cx: &App) -> Task> { + pub fn checkpoint(&self, cx: &mut App) -> Task> { let mut work_directory_abs_paths = Vec::new(); let mut checkpoints = Vec::new(); for repository in self.repositories.values() { - let repository = repository.read(cx); - work_directory_abs_paths.push(repository.snapshot.work_directory_abs_path.clone()); - checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?)); + repository.update(cx, |repository, _| { + work_directory_abs_paths.push(repository.snapshot.work_directory_abs_path.clone()); + checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?)); + }); } cx.background_executor().spawn(async move { @@ -729,7 +752,11 @@ impl GitStore { }) } - pub fn restore_checkpoint(&self, checkpoint: GitStoreCheckpoint, cx: &App) -> Task> { + pub fn restore_checkpoint( + &self, + checkpoint: GitStoreCheckpoint, + cx: &mut App, + ) -> Task> { let repositories_by_work_dir_abs_path = self .repositories .values() @@ -739,7 +766,9 @@ impl GitStore { let mut tasks = Vec::new(); for (work_dir_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path { if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) { - let restore = repository.read(cx).restore_checkpoint(checkpoint); + let restore = repository.update(cx, |repository, _| { + repository.restore_checkpoint(checkpoint) + }); tasks.push(async move { restore.await? }); } } @@ -754,7 +783,7 @@ impl GitStore { &self, left: GitStoreCheckpoint, mut right: GitStoreCheckpoint, - cx: &App, + cx: &mut App, ) -> Task> { let repositories_by_work_dir_abs_path = self .repositories @@ -770,9 +799,10 @@ impl GitStore { { if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) { - let compare = repository - .read(cx) - .compare_checkpoints(left_checkpoint, right_checkpoint); + let compare = repository.update(cx, |repository, _| { + repository.compare_checkpoints(left_checkpoint, right_checkpoint) + }); + tasks.push(async move { compare.await? }); } } else { @@ -787,7 +817,11 @@ impl GitStore { }) } - pub fn delete_checkpoint(&self, checkpoint: GitStoreCheckpoint, cx: &App) -> Task> { + pub fn delete_checkpoint( + &self, + checkpoint: GitStoreCheckpoint, + cx: &mut App, + ) -> Task> { let repositories_by_work_directory_abs_path = self .repositories .values() @@ -799,7 +833,7 @@ impl GitStore { if let Some(repository) = repositories_by_work_directory_abs_path.get(&work_dir_abs_path) { - let delete = repository.read(cx).delete_checkpoint(checkpoint); + let delete = repository.update(cx, |this, _| this.delete_checkpoint(checkpoint)); tasks.push(async move { delete.await? }); } } @@ -814,7 +848,7 @@ impl GitStore { &self, buffer: &Entity, version: Option, - cx: &App, + cx: &mut App, ) -> Task>> { let buffer = buffer.read(cx); let Some((repo, repo_path)) = @@ -829,24 +863,26 @@ impl GitStore { let version = version.unwrap_or(buffer.version()); let buffer_id = buffer.remote_id(); - let rx = repo.read(cx).send_job(move |state, _| async move { - match state { - RepositoryState::Local { backend, .. } => backend - .blame(repo_path.clone(), content) - .await - .with_context(|| format!("Failed to blame {:?}", repo_path.0)) - .map(Some), - RepositoryState::Remote { project_id, client } => { - let response = client - .request(proto::BlameBuffer { - project_id: project_id.to_proto(), - buffer_id: buffer_id.into(), - version: serialize_version(&version), - }) - .await?; - Ok(deserialize_blame_buffer_response(response)) + let rx = repo.update(cx, |repo, _| { + repo.send_job(None, move |state, _| async move { + match state { + RepositoryState::Local { backend, .. } => backend + .blame(repo_path.clone(), content) + .await + .with_context(|| format!("Failed to blame {:?}", repo_path.0)) + .map(Some), + RepositoryState::Remote { project_id, client } => { + let response = client + .request(proto::BlameBuffer { + project_id: project_id.to_proto(), + buffer_id: buffer_id.into(), + version: serialize_version(&version), + }) + .await?; + Ok(deserialize_blame_buffer_response(response)) + } } - } + }) }); cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) @@ -856,7 +892,7 @@ impl GitStore { &self, buffer: &Entity, selection: Range, - cx: &App, + cx: &mut App, ) -> Task> { let Some(file) = File::from_dyn(buffer.read(cx).file()) else { return Task::ready(Err(anyhow!("buffer has no file"))); @@ -897,52 +933,55 @@ impl GitStore { .and_then(|b| b.remote_name()) .unwrap_or("origin") .to_string(); - let rx = repo.read(cx).send_job(move |state, cx| async move { - match state { - RepositoryState::Local { backend, .. } => { - let origin_url = backend - .remote_url(&remote) - .ok_or_else(|| anyhow!("remote \"{remote}\" not found"))?; - let sha = backend - .head_sha() - .ok_or_else(|| anyhow!("failed to read HEAD SHA"))?; + let rx = repo.update(cx, |repo, _| { + repo.send_job(None, move |state, cx| async move { + match state { + RepositoryState::Local { backend, .. } => { + let origin_url = backend + .remote_url(&remote) + .ok_or_else(|| anyhow!("remote \"{remote}\" not found"))?; - let provider_registry = - cx.update(GitHostingProviderRegistry::default_global)?; + let sha = backend + .head_sha() + .ok_or_else(|| anyhow!("failed to read HEAD SHA"))?; - let (provider, remote) = - parse_git_remote_url(provider_registry, &origin_url) - .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?; + let provider_registry = + cx.update(GitHostingProviderRegistry::default_global)?; - let path = repo_path - .to_str() - .ok_or_else(|| anyhow!("failed to convert path to string"))?; + let (provider, remote) = + parse_git_remote_url(provider_registry, &origin_url) + .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?; - Ok(provider.build_permalink( - remote, - BuildPermalinkParams { - sha: &sha, - path, - selection: Some(selection), - }, - )) + let path = repo_path + .to_str() + .ok_or_else(|| anyhow!("failed to convert path to string"))?; + + Ok(provider.build_permalink( + remote, + BuildPermalinkParams { + sha: &sha, + path, + selection: Some(selection), + }, + )) + } + RepositoryState::Remote { project_id, client } => { + let response = client + .request(proto::GetPermalinkToLine { + project_id: project_id.to_proto(), + buffer_id: buffer_id.into(), + selection: Some(proto::Range { + start: selection.start as u64, + end: selection.end as u64, + }), + }) + .await?; + + url::Url::parse(&response.permalink).context("failed to parse permalink") + } } - RepositoryState::Remote { project_id, client } => { - let response = client - .request(proto::GetPermalinkToLine { - project_id: project_id.to_proto(), - buffer_id: buffer_id.into(), - selection: Some(proto::Range { - start: selection.start as u64, - end: selection.end as u64, - }), - }) - .await?; - - url::Url::parse(&response.permalink).context("failed to parse permalink") - } - } + }) }); cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) } @@ -1058,6 +1097,10 @@ impl GitStore { )) } + fn on_jobs_updated(&mut self, _: Entity, _: &JobsUpdated, cx: &mut Context) { + cx.emit(GitStoreEvent::JobsUpdated) + } + /// Update our list of repositories and schedule git scans in response to a notification from a worktree, fn update_repositories_from_worktree( &mut self, @@ -1110,6 +1153,8 @@ impl GitStore { }); self._subscriptions .push(cx.subscribe(&repo, Self::on_repository_event)); + self._subscriptions + .push(cx.subscribe(&repo, Self::on_jobs_updated)); self.repositories.insert(id, repo); cx.emit(GitStoreEvent::RepositoryAdded(id)); self.active_repo_id.get_or_insert_with(|| { @@ -1291,124 +1336,127 @@ impl GitStore { for (repo, repo_diff_state_updates) in diff_state_updates.into_iter() { let git_store = cx.weak_entity(); - let _ = repo.read(cx).send_keyed_job( - Some(GitJobKey::BatchReadIndex), - |state, mut cx| async move { - let RepositoryState::Local { backend, .. } = state else { - log::error!("tried to recompute diffs for a non-local repository"); - return; - }; - let mut diff_bases_changes_by_buffer = Vec::new(); - for ( - buffer, - repo_path, - current_index_text, - current_head_text, - hunk_staging_operation_count, - ) in &repo_diff_state_updates - { - let index_text = if current_index_text.is_some() { - backend.load_index_text(repo_path.clone()).await - } else { - None + let _ = repo.update(cx, |repo, _| { + repo.send_keyed_job( + Some(GitJobKey::BatchReadIndex), + None, + |state, mut cx| async move { + let RepositoryState::Local { backend, .. } = state else { + log::error!("tried to recompute diffs for a non-local repository"); + return; }; - let head_text = if current_head_text.is_some() { - backend.load_committed_text(repo_path.clone()).await - } else { - None - }; - - // Avoid triggering a diff update if the base text has not changed. - if let Some((current_index, current_head)) = - current_index_text.as_ref().zip(current_head_text.as_ref()) + let mut diff_bases_changes_by_buffer = Vec::new(); + for ( + buffer, + repo_path, + current_index_text, + current_head_text, + hunk_staging_operation_count, + ) in &repo_diff_state_updates { - if current_index.as_deref() == index_text.as_ref() - && current_head.as_deref() == head_text.as_ref() - { - continue; - } - } - - let diff_bases_change = - match (current_index_text.is_some(), current_head_text.is_some()) { - (true, true) => Some(if index_text == head_text { - DiffBasesChange::SetBoth(head_text) - } else { - DiffBasesChange::SetEach { - index: index_text, - head: head_text, - } - }), - (true, false) => Some(DiffBasesChange::SetIndex(index_text)), - (false, true) => Some(DiffBasesChange::SetHead(head_text)), - (false, false) => None, + let index_text = if current_index_text.is_some() { + backend.load_index_text(repo_path.clone()).await + } else { + None + }; + let head_text = if current_head_text.is_some() { + backend.load_committed_text(repo_path.clone()).await + } else { + None }; - diff_bases_changes_by_buffer.push(( - buffer, - diff_bases_change, - *hunk_staging_operation_count, - )) - } - - git_store - .update(&mut cx, |git_store, cx| { - for (buffer, diff_bases_change, hunk_staging_operation_count) in - diff_bases_changes_by_buffer + // Avoid triggering a diff update if the base text has not changed. + if let Some((current_index, current_head)) = + current_index_text.as_ref().zip(current_head_text.as_ref()) { - let Some(diff_state) = - git_store.diffs.get(&buffer.read(cx).remote_id()) - else { + if current_index.as_deref() == index_text.as_ref() + && current_head.as_deref() == head_text.as_ref() + { continue; - }; - let Some(diff_bases_change) = diff_bases_change else { - continue; - }; - - let downstream_client = git_store.downstream_client(); - diff_state.update(cx, |diff_state, cx| { - use proto::update_diff_bases::Mode; - - let buffer = buffer.read(cx); - if let Some((client, project_id)) = downstream_client { - let (staged_text, committed_text, mode) = - match diff_bases_change.clone() { - DiffBasesChange::SetIndex(index) => { - (index, None, Mode::IndexOnly) - } - DiffBasesChange::SetHead(head) => { - (None, head, Mode::HeadOnly) - } - DiffBasesChange::SetEach { index, head } => { - (index, head, Mode::IndexAndHead) - } - DiffBasesChange::SetBoth(text) => { - (None, text, Mode::IndexMatchesHead) - } - }; - let message = proto::UpdateDiffBases { - project_id: project_id.to_proto(), - buffer_id: buffer.remote_id().to_proto(), - staged_text, - committed_text, - mode: mode as i32, - }; - - client.send(message).log_err(); - } - - let _ = diff_state.diff_bases_changed( - buffer.text_snapshot(), - diff_bases_change, - hunk_staging_operation_count, - cx, - ); - }); + } } - }) - .ok(); - }, - ); + + let diff_bases_change = + match (current_index_text.is_some(), current_head_text.is_some()) { + (true, true) => Some(if index_text == head_text { + DiffBasesChange::SetBoth(head_text) + } else { + DiffBasesChange::SetEach { + index: index_text, + head: head_text, + } + }), + (true, false) => Some(DiffBasesChange::SetIndex(index_text)), + (false, true) => Some(DiffBasesChange::SetHead(head_text)), + (false, false) => None, + }; + + diff_bases_changes_by_buffer.push(( + buffer, + diff_bases_change, + *hunk_staging_operation_count, + )) + } + + git_store + .update(&mut cx, |git_store, cx| { + for (buffer, diff_bases_change, hunk_staging_operation_count) in + diff_bases_changes_by_buffer + { + let Some(diff_state) = + git_store.diffs.get(&buffer.read(cx).remote_id()) + else { + continue; + }; + let Some(diff_bases_change) = diff_bases_change else { + continue; + }; + + let downstream_client = git_store.downstream_client(); + diff_state.update(cx, |diff_state, cx| { + use proto::update_diff_bases::Mode; + + let buffer = buffer.read(cx); + if let Some((client, project_id)) = downstream_client { + let (staged_text, committed_text, mode) = + match diff_bases_change.clone() { + DiffBasesChange::SetIndex(index) => { + (index, None, Mode::IndexOnly) + } + DiffBasesChange::SetHead(head) => { + (None, head, Mode::HeadOnly) + } + DiffBasesChange::SetEach { index, head } => { + (index, head, Mode::IndexAndHead) + } + DiffBasesChange::SetBoth(text) => { + (None, text, Mode::IndexMatchesHead) + } + }; + let message = proto::UpdateDiffBases { + project_id: project_id.to_proto(), + buffer_id: buffer.remote_id().to_proto(), + staged_text, + committed_text, + mode: mode as i32, + }; + + client.send(message).log_err(); + } + + let _ = diff_state.diff_bases_changed( + buffer.text_snapshot(), + diff_bases_change, + hunk_staging_operation_count, + cx, + ); + }); + } + }) + .ok(); + }, + ) + }); } } @@ -2610,6 +2658,7 @@ impl Repository { ) -> Self { let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path.clone()); Repository { + this: cx.weak_entity(), git_store, snapshot, commit_message_buffer: None, @@ -2623,6 +2672,8 @@ impl Repository { fs, cx, ), + job_id: 0, + active_jobs: Default::default(), } } @@ -2636,6 +2687,7 @@ impl Repository { ) -> Self { let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path); Self { + this: cx.weak_entity(), snapshot, commit_message_buffer: None, git_store, @@ -2643,6 +2695,8 @@ impl Repository { job_sender: Self::spawn_remote_git_worker(project_id, client, cx), askpass_delegates: Default::default(), latest_askpass_id: 0, + active_jobs: Default::default(), + job_id: 0, } } @@ -2650,29 +2704,61 @@ impl Repository { self.git_store.upgrade() } - pub fn send_job(&self, job: F) -> oneshot::Receiver + pub fn send_job( + &mut self, + status: Option, + job: F, + ) -> oneshot::Receiver where F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static, Fut: Future + 'static, R: Send + 'static, { - self.send_keyed_job(None, job) + self.send_keyed_job(None, status, job) } - fn send_keyed_job(&self, key: Option, job: F) -> oneshot::Receiver + fn send_keyed_job( + &mut self, + key: Option, + status: Option, + job: F, + ) -> oneshot::Receiver where F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static, Fut: Future + 'static, R: Send + 'static, { let (result_tx, result_rx) = futures::channel::oneshot::channel(); + let job_id = post_inc(&mut self.job_id); + let this = self.this.clone(); self.job_sender .unbounded_send(GitJob { key, - job: Box::new(|state, cx: &mut AsyncApp| { + job: Box::new(move |state, cx: &mut AsyncApp| { let job = job(state, cx.clone()); - cx.spawn(async move |_| { + cx.spawn(async move |cx| { + if let Some(s) = status.clone() { + this.update(cx, |this, cx| { + this.active_jobs.insert( + job_id, + JobInfo { + start: Instant::now(), + message: s.clone(), + }, + ); + + cx.notify(); + }) + .ok(); + } let result = job.await; + + this.update(cx, |this, cx| { + this.active_jobs.remove(&job_id); + cx.notify(); + }) + .ok(); + result_tx.send(result).ok(); }) }), @@ -2741,7 +2827,7 @@ impl Repository { } let this = cx.weak_entity(); - let rx = self.send_job(move |state, mut cx| async move { + let rx = self.send_job(None, move |state, mut cx| async move { let Some(this) = this.upgrade() else { bail!("git store was dropped"); }; @@ -2807,7 +2893,7 @@ impl Repository { } pub fn checkout_files( - &self, + &mut self, commit: &str, paths: Vec, _cx: &mut App, @@ -2815,38 +2901,41 @@ impl Repository { let commit = commit.to_string(); let id = self.id; - self.send_job(move |git_repo, _| async move { - match git_repo { - RepositoryState::Local { - backend, - environment, - .. - } => { - backend - .checkout_files(commit, paths, environment.clone()) - .await - } - RepositoryState::Remote { project_id, client } => { - client - .request(proto::GitCheckoutFiles { - project_id: project_id.0, - repository_id: id.to_proto(), - commit, - paths: paths - .into_iter() - .map(|p| p.to_string_lossy().to_string()) - .collect(), - }) - .await?; + self.send_job( + Some(format!("git checkout {}", commit).into()), + move |git_repo, _| async move { + match git_repo { + RepositoryState::Local { + backend, + environment, + .. + } => { + backend + .checkout_files(commit, paths, environment.clone()) + .await + } + RepositoryState::Remote { project_id, client } => { + client + .request(proto::GitCheckoutFiles { + project_id: project_id.0, + repository_id: id.to_proto(), + commit, + paths: paths + .into_iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(), + }) + .await?; - Ok(()) + Ok(()) + } } - } - }) + }, + ) } pub fn reset( - &self, + &mut self, commit: String, reset_mode: ResetMode, _cx: &mut App, @@ -2854,7 +2943,7 @@ impl Repository { let commit = commit.to_string(); let id = self.id; - self.send_job(move |git_repo, _| async move { + self.send_job(None, move |git_repo, _| async move { match git_repo { RepositoryState::Local { backend, @@ -2880,9 +2969,9 @@ impl Repository { }) } - pub fn show(&self, commit: String) -> oneshot::Receiver> { + pub fn show(&mut self, commit: String) -> oneshot::Receiver> { let id = self.id; - self.send_job(move |git_repo, _cx| async move { + self.send_job(None, move |git_repo, _cx| async move { match git_repo { RepositoryState::Local { backend, .. } => backend.show(commit).await, RepositoryState::Remote { project_id, client } => { @@ -2906,9 +2995,9 @@ impl Repository { }) } - pub fn load_commit_diff(&self, commit: String) -> oneshot::Receiver> { + pub fn load_commit_diff(&mut self, commit: String) -> oneshot::Receiver> { let id = self.id; - self.send_job(move |git_repo, cx| async move { + self.send_job(None, move |git_repo, cx| async move { match git_repo { RepositoryState::Local { backend, .. } => backend.load_commit(commit, cx).await, RepositoryState::Remote { @@ -2977,7 +3066,7 @@ impl Repository { } this.update(cx, |this, _| { - this.send_job(move |git_repo, _cx| async move { + this.send_job(None, move |git_repo, _cx| async move { match git_repo { RepositoryState::Local { backend, @@ -3044,7 +3133,7 @@ impl Repository { } this.update(cx, |this, _| { - this.send_job(move |git_repo, _cx| async move { + this.send_job(None, move |git_repo, _cx| async move { match git_repo { RepositoryState::Local { backend, @@ -3094,14 +3183,14 @@ impl Repository { } pub fn commit( - &self, + &mut self, message: SharedString, name_and_email: Option<(SharedString, SharedString)>, _cx: &mut App, ) -> oneshot::Receiver> { let id = self.id; - self.send_job(move |git_repo, _cx| async move { + self.send_job(Some("git commit".into()), move |git_repo, _cx| async move { match git_repo { RepositoryState::Local { backend, @@ -3136,7 +3225,7 @@ impl Repository { let askpass_id = util::post_inc(&mut self.latest_askpass_id); let id = self.id; - self.send_job(move |git_repo, cx| async move { + self.send_job(Some("git fetch".into()), move |git_repo, cx| async move { match git_repo { RepositoryState::Local { backend, @@ -3180,52 +3269,65 @@ impl Repository { let askpass_id = util::post_inc(&mut self.latest_askpass_id); let id = self.id; - self.send_job(move |git_repo, cx| async move { - match git_repo { - RepositoryState::Local { - backend, - environment, - .. - } => { - backend - .push( - branch.to_string(), - remote.to_string(), - options, - askpass, - environment.clone(), - cx, - ) - .await - } - RepositoryState::Remote { project_id, client } => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let response = client - .request(proto::Push { - project_id: project_id.0, - repository_id: id.to_proto(), - askpass_id, - branch_name: branch.to_string(), - remote_name: remote.to_string(), - options: options.map(|options| match options { - PushOptions::Force => proto::push::PushOptions::Force, - PushOptions::SetUpstream => proto::push::PushOptions::SetUpstream, - } as i32), - }) - .await - .context("sending push request")?; + let args = options + .map(|option| match option { + PushOptions::SetUpstream => " --set-upstream", + PushOptions::Force => " --force", + }) + .unwrap_or(""); - Ok(RemoteCommandOutput { - stdout: response.stdout, - stderr: response.stderr, - }) + self.send_job( + Some(format!("git push{} {} {}", args, branch, remote).into()), + move |git_repo, cx| async move { + match git_repo { + RepositoryState::Local { + backend, + environment, + .. + } => { + backend + .push( + branch.to_string(), + remote.to_string(), + options, + askpass, + environment.clone(), + cx, + ) + .await + } + RepositoryState::Remote { project_id, client } => { + askpass_delegates.lock().insert(askpass_id, askpass); + let _defer = util::defer(|| { + let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); + debug_assert!(askpass_delegate.is_some()); + }); + let response = client + .request(proto::Push { + project_id: project_id.0, + repository_id: id.to_proto(), + askpass_id, + branch_name: branch.to_string(), + remote_name: remote.to_string(), + options: options.map(|options| match options { + PushOptions::Force => proto::push::PushOptions::Force, + PushOptions::SetUpstream => { + proto::push::PushOptions::SetUpstream + } + } + as i32), + }) + .await + .context("sending push request")?; + + Ok(RemoteCommandOutput { + stdout: response.stdout, + stderr: response.stderr, + }) + } } - } - }) + }, + ) } pub fn pull( @@ -3239,51 +3341,54 @@ impl Repository { let askpass_id = util::post_inc(&mut self.latest_askpass_id); let id = self.id; - self.send_job(move |git_repo, cx| async move { - match git_repo { - RepositoryState::Local { - backend, - environment, - .. - } => { - backend - .pull( - branch.to_string(), - remote.to_string(), - askpass, - environment.clone(), - cx, - ) - .await - } - RepositoryState::Remote { project_id, client } => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let response = client - .request(proto::Pull { - project_id: project_id.0, - repository_id: id.to_proto(), - askpass_id, - branch_name: branch.to_string(), - remote_name: remote.to_string(), - }) - .await - .context("sending pull request")?; + self.send_job( + Some(format!("git pull {} {}", remote, branch).into()), + move |git_repo, cx| async move { + match git_repo { + RepositoryState::Local { + backend, + environment, + .. + } => { + backend + .pull( + branch.to_string(), + remote.to_string(), + askpass, + environment.clone(), + cx, + ) + .await + } + RepositoryState::Remote { project_id, client } => { + askpass_delegates.lock().insert(askpass_id, askpass); + let _defer = util::defer(|| { + let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); + debug_assert!(askpass_delegate.is_some()); + }); + let response = client + .request(proto::Pull { + project_id: project_id.0, + repository_id: id.to_proto(), + askpass_id, + branch_name: branch.to_string(), + remote_name: remote.to_string(), + }) + .await + .context("sending pull request")?; - Ok(RemoteCommandOutput { - stdout: response.stdout, - stderr: response.stderr, - }) + Ok(RemoteCommandOutput { + stdout: response.stdout, + stderr: response.stderr, + }) + } } - } - }) + }, + ) } fn spawn_set_index_text_job( - &self, + &mut self, path: RepoPath, content: Option, _cx: &mut App, @@ -3292,6 +3397,7 @@ impl Repository { self.send_keyed_job( Some(GitJobKey::WriteIndex(path.clone())), + None, move |git_repo, _cx| async move { match git_repo { RepositoryState::Local { @@ -3320,11 +3426,11 @@ impl Repository { } pub fn get_remotes( - &self, + &mut self, branch_name: Option, ) -> oneshot::Receiver>> { let id = self.id; - self.send_job(move |repo, _cx| async move { + self.send_job(None, move |repo, _cx| async move { match repo { RepositoryState::Local { backend, .. } => backend.get_remotes(branch_name).await, RepositoryState::Remote { project_id, client } => { @@ -3350,9 +3456,9 @@ impl Repository { }) } - pub fn branches(&self) -> oneshot::Receiver>> { + pub fn branches(&mut self) -> oneshot::Receiver>> { let id = self.id; - self.send_job(move |repo, cx| async move { + self.send_job(None, move |repo, cx| async move { match repo { RepositoryState::Local { backend, .. } => { let backend = backend.clone(); @@ -3379,9 +3485,9 @@ impl Repository { }) } - pub fn diff(&self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver> { + pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver> { let id = self.id; - self.send_job(move |repo, _cx| async move { + self.send_job(None, move |repo, _cx| async move { match repo { RepositoryState::Local { backend, .. } => backend.diff(diff_type).await, RepositoryState::Remote { project_id, client } => { @@ -3406,49 +3512,59 @@ impl Repository { }) } - pub fn create_branch(&self, branch_name: String) -> oneshot::Receiver> { + pub fn create_branch(&mut self, branch_name: String) -> oneshot::Receiver> { let id = self.id; - self.send_job(move |repo, _cx| async move { - match repo { - RepositoryState::Local { backend, .. } => backend.create_branch(branch_name).await, - RepositoryState::Remote { project_id, client } => { - client - .request(proto::GitCreateBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - }) - .await?; + self.send_job( + Some(format!("git switch -c {branch_name}").into()), + move |repo, _cx| async move { + match repo { + RepositoryState::Local { backend, .. } => { + backend.create_branch(branch_name).await + } + RepositoryState::Remote { project_id, client } => { + client + .request(proto::GitCreateBranch { + project_id: project_id.0, + repository_id: id.to_proto(), + branch_name, + }) + .await?; - Ok(()) + Ok(()) + } } - } - }) + }, + ) } - pub fn change_branch(&self, branch_name: String) -> oneshot::Receiver> { + pub fn change_branch(&mut self, branch_name: String) -> oneshot::Receiver> { let id = self.id; - self.send_job(move |repo, _cx| async move { - match repo { - RepositoryState::Local { backend, .. } => backend.change_branch(branch_name).await, - RepositoryState::Remote { project_id, client } => { - client - .request(proto::GitChangeBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - }) - .await?; + self.send_job( + Some(format!("git switch {branch_name}").into()), + move |repo, _cx| async move { + match repo { + RepositoryState::Local { backend, .. } => { + backend.change_branch(branch_name).await + } + RepositoryState::Remote { project_id, client } => { + client + .request(proto::GitChangeBranch { + project_id: project_id.0, + repository_id: id.to_proto(), + branch_name, + }) + .await?; - Ok(()) + Ok(()) + } } - } - }) + }, + ) } - pub fn check_for_pushed_commits(&self) -> oneshot::Receiver>> { + pub fn check_for_pushed_commits(&mut self) -> oneshot::Receiver>> { let id = self.id; - self.send_job(move |repo, _cx| async move { + self.send_job(None, move |repo, _cx| async move { match repo { RepositoryState::Local { backend, .. } => backend.check_for_pushed_commit().await, RepositoryState::Remote { project_id, client } => { @@ -3467,8 +3583,8 @@ impl Repository { }) } - pub fn checkpoint(&self) -> oneshot::Receiver> { - self.send_job(|repo, _cx| async move { + pub fn checkpoint(&mut self) -> oneshot::Receiver> { + self.send_job(None, |repo, _cx| async move { match repo { RepositoryState::Local { backend, .. } => backend.checkpoint().await, RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")), @@ -3477,10 +3593,10 @@ impl Repository { } pub fn restore_checkpoint( - &self, + &mut self, checkpoint: GitRepositoryCheckpoint, ) -> oneshot::Receiver> { - self.send_job(move |repo, _cx| async move { + self.send_job(None, move |repo, _cx| async move { match repo { RepositoryState::Local { backend, .. } => { backend.restore_checkpoint(checkpoint).await @@ -3526,11 +3642,11 @@ impl Repository { } pub fn compare_checkpoints( - &self, + &mut self, left: GitRepositoryCheckpoint, right: GitRepositoryCheckpoint, ) -> oneshot::Receiver> { - self.send_job(move |repo, _cx| async move { + self.send_job(None, move |repo, _cx| async move { match repo { RepositoryState::Local { backend, .. } => { backend.compare_checkpoints(left, right).await @@ -3541,10 +3657,10 @@ impl Repository { } pub fn delete_checkpoint( - &self, + &mut self, checkpoint: GitRepositoryCheckpoint, ) -> oneshot::Receiver> { - self.send_job(move |repo, _cx| async move { + self.send_job(None, move |repo, _cx| async move { match repo { RepositoryState::Local { backend, .. } => { backend.delete_checkpoint(checkpoint).await @@ -3555,11 +3671,11 @@ impl Repository { } pub fn diff_checkpoints( - &self, + &mut self, base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, ) -> oneshot::Receiver> { - self.send_job(move |repo, _cx| async move { + self.send_job(None, move |repo, _cx| async move { match repo { RepositoryState::Local { backend, .. } => { backend @@ -3585,6 +3701,7 @@ impl Repository { let this = cx.weak_entity(); let _ = self.send_keyed_job( Some(GitJobKey::ReloadGitState), + None, |state, mut cx| async move { let Some(this) = this.upgrade() else { return Ok(()); @@ -3727,12 +3844,12 @@ impl Repository { } fn load_staged_text( - &self, + &mut self, buffer_id: BufferId, repo_path: RepoPath, cx: &App, ) -> Task>> { - let rx = self.send_job(move |state, _| async move { + let rx = self.send_job(None, move |state, _| async move { match state { RepositoryState::Local { backend, .. } => { anyhow::Ok(backend.load_index_text(repo_path).await) @@ -3752,12 +3869,12 @@ impl Repository { } fn load_committed_text( - &self, + &mut self, buffer_id: BufferId, repo_path: RepoPath, cx: &App, ) -> Task> { - let rx = self.send_job(move |state, _| async move { + let rx = self.send_job(None, move |state, _| async move { match state { RepositoryState::Local { backend, .. } => { let committed_text = backend.load_committed_text(repo_path.clone()).await; @@ -3809,6 +3926,7 @@ impl Repository { let this = cx.weak_entity(); let _ = self.send_keyed_job( Some(GitJobKey::RefreshStatuses), + None, |state, mut cx| async move { let (prev_snapshot, mut changed_paths) = this.update(&mut cx, |this, _| { ( @@ -3871,6 +3989,11 @@ impl Repository { }, ); } + + /// currently running git command and when it started + pub fn current_job(&self) -> Option { + self.active_jobs.values().next().cloned() + } } fn get_permalink_in_rust_registry_src( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9e68bcf397..f2537f4c04 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4135,20 +4135,22 @@ impl Project { &self, buffer: &Entity, version: Option, - cx: &App, + cx: &mut App, ) -> Task>> { - self.git_store.read(cx).blame_buffer(buffer, version, cx) + self.git_store.update(cx, |git_store, cx| { + git_store.blame_buffer(buffer, version, cx) + }) } pub fn get_permalink_to_line( &self, buffer: &Entity, selection: Range, - cx: &App, + cx: &mut App, ) -> Task> { - self.git_store - .read(cx) - .get_permalink_to_line(buffer, selection, cx) + self.git_store.update(cx, |git_store, cx| { + git_store.get_permalink_to_line(buffer, selection, cx) + }) } // RPC message handlers diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 9b5a98bf38..bb65156658 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1366,10 +1366,14 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA assert_eq!(&remote_branches, &branches_set); - cx.update(|cx| repository.read(cx).change_branch(new_branch.to_string())) - .await - .unwrap() - .unwrap(); + cx.update(|cx| { + repository.update(cx, |repository, _cx| { + repository.change_branch(new_branch.to_string()) + }) + }) + .await + .unwrap() + .unwrap(); cx.run_until_parked(); @@ -1394,18 +1398,18 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA // Also try creating a new branch cx.update(|cx| { - repository - .read(cx) - .create_branch("totally-new-branch".to_string()) + repository.update(cx, |repo, _cx| { + repo.create_branch("totally-new-branch".to_string()) + }) }) .await .unwrap() .unwrap(); cx.update(|cx| { - repository - .read(cx) - .change_branch("totally-new-branch".to_string()) + repository.update(cx, |repo, _cx| { + repo.change_branch("totally-new-branch".to_string()) + }) }) .await .unwrap()