Skip to content

Indicate to the user that a stash save is in progress #1702

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions asyncgit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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},
Expand All @@ -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,
Expand Down
77 changes: 77 additions & 0 deletions asyncgit/src/stash.rs
Original file line number Diff line number Diff line change
@@ -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<AtomicUsize>,
sender: Sender<AsyncGitNotification>,
repo: RepoPath,
}

impl AsyncStash {
///
pub fn new(
repo: RepoPath,
sender: Sender<AsyncGitNotification>,
) -> 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<Option<CommitId>> {
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
}
}
2 changes: 1 addition & 1 deletion asyncgit/src/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ mod repository;
mod reset;
mod reword;
mod staging;
mod stash;
pub mod stash;
mod state;
pub mod status;
mod submodules;
Expand Down
2 changes: 2 additions & 0 deletions asyncgit/src/sync/stash.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! sync git api for stash

use super::{CommitId, RepoPath};
use crate::{
error::{Error, Result},
Expand Down
5 changes: 4 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,9 @@ impl App {
key_config.clone(),
),
stashmsg_popup: StashMsgComponent::new(
repo.clone(),
&repo,
queue.clone(),
sender,
theme.clone(),
key_config.clone(),
),
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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()
Expand Down
66 changes: 34 additions & 32 deletions src/components/stashmsg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -64,40 +65,21 @@ 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 {
Some(self.input.get_text())
},
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
Expand Down Expand Up @@ -125,8 +107,9 @@ impl Component for StashMsgComponent {
impl StashMsgComponent {
///
pub fn new(
repo: RepoPathRef,
repo: &RepoPathRef,
queue: Queue,
sender: &Sender<AsyncGitNotification>,
theme: SharedTheme,
key_config: SharedKeyConfig,
) -> Self {
Expand All @@ -141,12 +124,31 @@ impl StashMsgComponent {
true,
),
key_config,
repo,
git_stash: AsyncStash::new(
repo.borrow().clone(),
sender.clone(),
),
}
}

///
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));
}
}
}
15 changes: 14 additions & 1 deletion src/components/textinput.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -57,6 +59,7 @@ impl TextInputComponent {
Self {
msg: String::new(),
visible: false,
disabled: false,
theme,
key_config,
show_char_count,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -419,7 +432,7 @@ impl Component for TextInputComponent {
}

fn event(&mut self, ev: &Event) -> Result<EventState> {
if self.visible {
if self.visible && !self.disabled {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is pretty invasive. why do we need that? the text input for the stashing can just close IMO and does not need to learn a new state of disabled. what is the rational?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without disabled, it's possible to type in the field while the operation is pending.

I suppose we could close the model and continue the operation in the background. In this case, is there a pattern for handling stashing errors if they occur?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that's what I think we should do, what we do in other cases already. just close the text input popup regularly and then the new popup will block any other input and visualise that the stashing is in progress

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for error handling use try_or_popup and find other examples for it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll make that change soon 👍🏻

if let Event::Key(e) = ev {
if key_match(e, self.key_config.keys.exit_popup) {
self.hide();
Expand Down
3 changes: 3 additions & 0 deletions src/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down