diff --git a/CHANGELOG.md b/CHANGELOG.md index 373b82216a..13f2c4ef39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * show remote branches in log [[@cruessler](https://github.com/cruessler)] ([#1501](https://github.com/extrawurst/gitui/issues/1501)) ### Fixes +* fixed git stash not being async [[@nrempel](https://github.com/nrempel)] ([#1702](https://github.com/extrawurst/gitui/issues/1702)) * fixed side effect of crossterm 0.26 on windows that caused double input of all keys [[@pm100]](https://github/pm100) ([#1686](https://github.com/extrawurst/gitui/pull/1686)) * commit msg history ordered the wrong way ([#1445](https://github.com/extrawurst/gitui/issues/1445)) * improve help documentation for amend cmd ([#1448](https://github.com/extrawurst/gitui/issues/1448)) diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index ecf85e0c71..322baefb68 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -39,6 +39,7 @@ mod push_tags; pub mod remote_progress; pub mod remote_tags; mod revlog; +mod stash; mod status; pub mod sync; mod tags; @@ -57,6 +58,7 @@ pub use crate::{ push_tags::{AsyncPushTags, PushTagsRequest}, remote_progress::{RemoteProgress, RemoteProgressState}, revlog::{AsyncLog, FetchStatus}, + stash::AsyncStash, status::{AsyncStatus, StatusParams}, sync::{ diff::{DiffLine, DiffLineType, FileDiff}, @@ -78,6 +80,8 @@ pub enum AsyncGitNotification { /// this indicates that no new state was fetched but that a async process finished FinishUnchanged, /// + Stash, + /// Status, /// Diff, diff --git a/asyncgit/src/stash.rs b/asyncgit/src/stash.rs new file mode 100644 index 0000000000..bc9dc3435d --- /dev/null +++ b/asyncgit/src/stash.rs @@ -0,0 +1,77 @@ +use crate::{ + error::Result, + sync::{self, CommitId, RepoPath}, + AsyncGitNotification, +}; +use crossbeam_channel::Sender; +use std::{ + sync::atomic::{AtomicUsize, Ordering}, + sync::Arc, +}; + +/// +pub struct AsyncStash { + pending: Arc, + sender: Sender, + repo: RepoPath, +} + +impl AsyncStash { + /// + pub fn new( + repo: RepoPath, + sender: Sender, + ) -> Self { + Self { + repo, + sender, + pending: Arc::new(AtomicUsize::new(0)), + } + } + + /// + pub fn stash_save( + &mut self, + message: Option<&str>, + include_untracked: bool, + keep_index: bool, + ) -> Result> { + if self.is_pending() { + log::trace!("request blocked, still pending"); + return Ok(None); + } + + let repo = self.repo.clone(); + let sender = self.sender.clone(); + let pending = self.pending.clone(); + let message = message.map(ToOwned::to_owned); + + self.pending.fetch_add(1, Ordering::Relaxed); + + rayon_core::spawn(move || { + let res = sync::stash::stash_save( + &repo, + message.as_deref(), + include_untracked, + keep_index, + ); + + pending.fetch_sub(1, Ordering::Relaxed); + + sender + .send(AsyncGitNotification::Stash) + .expect("error sending stash notification"); + + if let Err(e) = res { + log::error!("AsyncStash error: {}", e); + } + }); + + Ok(None) + } + + /// + pub fn is_pending(&self) -> bool { + self.pending.load(Ordering::Relaxed) > 0 + } +} diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index a7bf7d64ed..1e2bb70947 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -25,7 +25,7 @@ mod repository; mod reset; mod reword; mod staging; -mod stash; +pub mod stash; mod state; pub mod status; mod submodules; diff --git a/asyncgit/src/sync/stash.rs b/asyncgit/src/sync/stash.rs index f95f2f53b1..9062fd43c9 100644 --- a/asyncgit/src/sync/stash.rs +++ b/asyncgit/src/sync/stash.rs @@ -1,3 +1,5 @@ +//! sync git api for stash + use super::{CommitId, RepoPath}; use crate::{ error::{Error, Result}, diff --git a/src/app.rs b/src/app.rs index 26ce4647bd..35a6794979 100644 --- a/src/app.rs +++ b/src/app.rs @@ -169,8 +169,9 @@ impl App { key_config.clone(), ), stashmsg_popup: StashMsgComponent::new( - repo.clone(), + &repo, queue.clone(), + sender, theme.clone(), key_config.clone(), ), @@ -514,6 +515,7 @@ impl App { if let AsyncNotification::Git(ev) = ev { self.status_tab.update_git(ev)?; self.stashing_tab.update_git(ev)?; + self.stashmsg_popup.update_git(ev); self.revlog.update_git(ev)?; self.blame_file_popup.update_git(ev)?; self.file_revlog_popup.update_git(ev)?; @@ -553,6 +555,7 @@ impl App { self.status_tab.anything_pending() || self.revlog.any_work_pending() || self.stashing_tab.anything_pending() + || self.stashmsg_popup.anything_pending() || self.files_tab.anything_pending() || self.blame_file_popup.any_work_pending() || self.file_revlog_popup.any_work_pending() diff --git a/src/components/stashmsg.rs b/src/components/stashmsg.rs index bd73931142..7a6b73ec27 100644 --- a/src/components/stashmsg.rs +++ b/src/components/stashmsg.rs @@ -6,19 +6,20 @@ use super::{ use crate::{ keys::{key_match, SharedKeyConfig}, queue::{InternalEvent, NeedsUpdate, Queue}, - strings, + strings::{self}, tabs::StashingOptions, ui::style::SharedTheme, }; use anyhow::Result; -use asyncgit::sync::{self, RepoPathRef}; +use asyncgit::{sync::RepoPathRef, AsyncGitNotification, AsyncStash}; +use crossbeam_channel::Sender; use crossterm::event::Event; use ratatui::{backend::Backend, layout::Rect, Frame}; pub struct StashMsgComponent { - repo: RepoPathRef, options: StashingOptions, input: TextInputComponent, + git_stash: AsyncStash, queue: Queue, key_config: SharedKeyConfig, } @@ -64,8 +65,13 @@ impl Component for StashMsgComponent { if let Event::Key(e) = ev { if key_match(e, self.key_config.keys.enter) { - let result = sync::stash_save( - &self.repo.borrow(), + self.input.disable(); + self.input.set_title( + strings::stash_popup_stashing( + &self.key_config, + ), + ); + self.git_stash.stash_save( if self.input.get_text().is_empty() { None } else { @@ -73,31 +79,7 @@ impl Component for StashMsgComponent { }, self.options.stash_untracked, self.options.keep_index, - ); - match result { - Ok(_) => { - self.input.clear(); - self.hide(); - - self.queue.push(InternalEvent::Update( - NeedsUpdate::ALL, - )); - } - Err(e) => { - self.hide(); - log::error!( - "e: {} (options: {:?})", - e, - self.options - ); - self.queue.push( - InternalEvent::ShowErrorMsg(format!( - "stash error:\n{}\noptions:\n{:?}", - e, self.options - )), - ); - } - } + )?; } // stop key event propagation @@ -125,8 +107,9 @@ impl Component for StashMsgComponent { impl StashMsgComponent { /// pub fn new( - repo: RepoPathRef, + repo: &RepoPathRef, queue: Queue, + sender: &Sender, theme: SharedTheme, key_config: SharedKeyConfig, ) -> Self { @@ -141,7 +124,10 @@ impl StashMsgComponent { true, ), key_config, - repo, + git_stash: AsyncStash::new( + repo.borrow().clone(), + sender.clone(), + ), } } @@ -149,4 +135,20 @@ impl StashMsgComponent { pub fn options(&mut self, options: StashingOptions) { self.options = options; } + + /// + pub fn anything_pending(&self) -> bool { + self.git_stash.is_pending() + } + + /// + pub fn update_git(&mut self, ev: AsyncGitNotification) { + if self.is_visible() && ev == AsyncGitNotification::Stash { + self.input.enable(); + self.input.clear(); + self.hide(); + + self.queue.push(InternalEvent::Update(NeedsUpdate::ALL)); + } + } } diff --git a/src/components/textinput.rs b/src/components/textinput.rs index 34547161ec..b00f75895a 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -31,11 +31,13 @@ pub enum InputType { } /// primarily a subcomponet for user input of text (used in `CommitComponent`) +#[allow(clippy::struct_excessive_bools)] pub struct TextInputComponent { title: String, default_msg: String, msg: String, visible: bool, + disabled: bool, show_char_count: bool, theme: SharedTheme, key_config: SharedKeyConfig, @@ -57,6 +59,7 @@ impl TextInputComponent { Self { msg: String::new(), visible: false, + disabled: false, theme, key_config, show_char_count, @@ -222,6 +225,16 @@ impl TextInputComponent { self.default_msg = v; } + /// + pub fn disable(&mut self) { + self.disabled = true; + } + + /// + pub fn enable(&mut self) { + self.disabled = false; + } + fn get_draw_text(&self) -> Text { let style = self.theme.text(true, false); @@ -419,7 +432,7 @@ impl Component for TextInputComponent { } fn event(&mut self, ev: &Event) -> Result { - if self.visible { + if self.visible && !self.disabled { if let Event::Key(e) = ev { if key_match(e, self.key_config.keys.exit_popup) { self.hide(); diff --git a/src/strings.rs b/src/strings.rs index fc7ef10760..0f0de87cc4 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -138,6 +138,9 @@ pub fn stash_popup_title(_key_config: &SharedKeyConfig) -> String { pub fn stash_popup_msg(_key_config: &SharedKeyConfig) -> String { "type name (optional)".to_string() } +pub fn stash_popup_stashing(_key_config: &SharedKeyConfig) -> String { + "Stashing...".to_string() +} pub fn confirm_title_reset() -> String { "Reset".to_string() }