diff --git a/CHANGELOG.md b/CHANGELOG.md index 97a832e2..08b82102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added -- push to origin support ([#265](https://github.com/extrawurst/gitui/issues/265)) +- push to remote ([#265](https://github.com/extrawurst/gitui/issues/265)) ([#267](https://github.com/extrawurst/gitui/issues/267)) + +![push](assets/push.gif) ### Changed - do not highlight selection in diff view when not focused ([#270](https://github.com/extrawurst/gitui/issues/270)) diff --git a/README.md b/README.md index c4174dbd..2816c052 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ - Inspect, commit, and amend changes (incl. hooks: _commit-msg_/_post-commit_) - Stage, unstage, revert and reset files and hunks - Stashing (save, apply, drop, and inspect) +- Push to remote - Browse commit log, diff committed changes - Scalable terminal UI layout - Async [input polling](assets/perf_compare.jpg) @@ -53,7 +54,7 @@ Over the last 2 years my go-to GUI tool for this was [fork](https://git-fork.com # Known Limitations -- no support for push and pull yet (see [#90](https://github.com/extrawurst/gitui/issues/90)) +- no support for `pull` yet (see [#90](https://github.com/extrawurst/gitui/issues/90)) - limited support for branching (see [#90](https://github.com/extrawurst/gitui/issues/91)) - no support for [bare repositories](https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server) (see [#100](https://github.com/extrawurst/gitui/issues/100)) - no support for [core.hooksPath](https://git-scm.com/docs/githooks) config diff --git a/assets/push.gif b/assets/push.gif new file mode 100644 index 00000000..8e0f7b07 Binary files /dev/null and b/assets/push.gif differ diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index e5076ad0..1755022f 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -20,7 +20,7 @@ mod tags; pub use crate::{ commit_files::AsyncCommitFiles, diff::{AsyncDiff, DiffParams, DiffType}, - push::{AsyncPush, PushRequest}, + push::{AsyncPush, PushProgress, PushProgressState, PushRequest}, revlog::{AsyncLog, FetchStatus}, status::{AsyncStatus, StatusParams}, sync::{ diff --git a/asyncgit/src/push.rs b/asyncgit/src/push.rs index 87dd9f8f..8fffa137 100644 --- a/asyncgit/src/push.rs +++ b/asyncgit/src/push.rs @@ -2,19 +2,83 @@ use crate::{ error::{Error, Result}, sync, AsyncNotification, CWD, }; -use crossbeam_channel::Sender; -use std::sync::{Arc, Mutex}; +use crossbeam_channel::{unbounded, Receiver, Sender}; +use git2::PackBuilderStage; +use std::{ + cmp, + sync::{Arc, Mutex}, + thread, + time::Duration, +}; +use sync::ProgressNotification; +use thread::JoinHandle; +/// #[derive(Clone, Debug)] -enum PushStates { - None, - // Packing, - // Pushing(usize, usize), +pub enum PushProgressState { + /// + PackingAddingObject, + /// + PackingDeltafiction, + /// + Pushing, } -impl Default for PushStates { - fn default() -> Self { - PushStates::None +/// +#[derive(Clone, Debug)] +pub struct PushProgress { + /// + pub state: PushProgressState, + /// + pub progress: u8, +} + +impl PushProgress { + /// + pub fn new( + state: PushProgressState, + current: usize, + total: usize, + ) -> Self { + let total = cmp::max(current, total); + let progress = current as f32 / total as f32 * 100.0; + let progress = progress as u8; + Self { state, progress } + } +} + +impl From for PushProgress { + fn from(progress: ProgressNotification) -> Self { + match progress { + ProgressNotification::Packing { + stage, + current, + total, + } => match stage { + PackBuilderStage::AddingObjects => PushProgress::new( + PushProgressState::PackingAddingObject, + current, + total, + ), + PackBuilderStage::Deltafication => PushProgress::new( + PushProgressState::PackingDeltafiction, + current, + total, + ), + }, + ProgressNotification::PushTransfer { + current, + total, + .. + } => PushProgress::new( + PushProgressState::Pushing, + current, + total, + ), + ProgressNotification::Done => { + PushProgress::new(PushProgressState::Pushing, 1, 1) + } + } } } @@ -30,13 +94,13 @@ pub struct PushRequest { #[derive(Default, Clone, Debug)] struct PushState { request: PushRequest, - state: PushStates, } /// pub struct AsyncPush { state: Arc>>, last_result: Arc>>, + progress: Arc>>, sender: Sender, } @@ -46,6 +110,7 @@ impl AsyncPush { Self { state: Arc::new(Mutex::new(None)), last_result: Arc::new(Mutex::new(None)), + progress: Arc::new(Mutex::new(None)), sender: sender.clone(), } } @@ -62,6 +127,12 @@ impl AsyncPush { Ok(res.clone()) } + /// + pub fn progress(&self) -> Result> { + let res = self.progress.lock()?; + Ok(res.map(|progress| progress.into())) + } + /// pub fn request(&mut self, params: PushRequest) -> Result<()> { log::trace!("request"); @@ -71,19 +142,35 @@ impl AsyncPush { } self.set_request(¶ms)?; + Self::set_progress(self.progress.clone(), None)?; let arc_state = Arc::clone(&self.state); let arc_res = Arc::clone(&self.last_result); + let arc_progress = Arc::clone(&self.progress); let sender = self.sender.clone(); - rayon_core::spawn(move || { - //TODO: use channels to communicate progress - let res = sync::push_origin( + thread::spawn(move || { + let (progress_sender, receiver) = unbounded(); + + let handle = Self::spawn_receiver_thread( + sender.clone(), + receiver, + arc_progress, + ); + + let res = sync::push( CWD, params.remote.as_str(), params.branch.as_str(), + progress_sender.clone(), ); + progress_sender + .send(ProgressNotification::Done) + .expect("closing send failed"); + + handle.join().expect("joining thread failed"); + Self::set_result(arc_res, res).expect("result error"); Self::clear_request(arc_state).expect("clear error"); @@ -96,6 +183,44 @@ impl AsyncPush { Ok(()) } + fn spawn_receiver_thread( + sender: Sender, + receiver: Receiver, + progress: Arc>>, + ) -> JoinHandle<()> { + log::info!("push progress receiver spawned"); + + thread::spawn(move || loop { + let incoming = receiver.recv(); + match incoming { + Ok(update) => { + Self::set_progress( + progress.clone(), + Some(update), + ) + .expect("set prgoress failed"); + sender + .send(AsyncNotification::Push) + .expect("error sending push"); + + //NOTE: for better debugging + thread::sleep(Duration::from_millis(300)); + + if let ProgressNotification::Done = update { + break; + } + } + Err(e) => { + log::error!( + "push progress receiver error: {}", + e + ); + break; + } + } + }) + } + fn set_request(&self, params: &PushRequest) -> Result<()> { let mut state = self.state.lock()?; @@ -105,7 +230,6 @@ impl AsyncPush { *state = Some(PushState { request: params.clone(), - ..PushState::default() }); Ok(()) @@ -121,6 +245,20 @@ impl AsyncPush { Ok(()) } + fn set_progress( + progress: Arc>>, + state: Option, + ) -> Result<()> { + let simple_progress: Option = + state.map(|prog| prog.into()); + log::info!("push progress: {:?}", simple_progress); + let mut progress = progress.lock()?; + + *progress = state; + + Ok(()) + } + fn set_result( arc_result: Arc>>, res: Result<()>, @@ -138,3 +276,24 @@ impl AsyncPush { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_progress_zero_total() { + let prog = + PushProgress::new(PushProgressState::Pushing, 1, 0); + + assert_eq!(prog.progress, 100); + } + + #[test] + fn test_progress_rounding() { + let prog = + PushProgress::new(PushProgressState::Pushing, 2, 10); + + assert_eq!(prog.progress, 20); + } +} diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 21c5041e..87251e39 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -30,7 +30,9 @@ pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult}; pub use hunks::{reset_hunk, stage_hunk, unstage_hunk}; pub use ignore::add_to_ignore; pub use logwalker::LogWalker; -pub use remotes::{fetch_origin, get_remotes, push_origin}; +pub use remotes::{ + fetch_origin, get_remotes, push, ProgressNotification, +}; pub use reset::{reset_stage, reset_workdir}; pub use stash::{get_stashes, stash_apply, stash_drop, stash_save}; pub use tags::{get_tags, CommitTags, Tags}; diff --git a/asyncgit/src/sync/remotes.rs b/asyncgit/src/sync/remotes.rs index 6e71e062..a572ced6 100644 --- a/asyncgit/src/sync/remotes.rs +++ b/asyncgit/src/sync/remotes.rs @@ -1,9 +1,38 @@ //! use crate::{error::Result, sync::utils}; -use git2::{Cred, FetchOptions, PushOptions, RemoteCallbacks}; +use crossbeam_channel::Sender; +use git2::{ + Cred, FetchOptions, PackBuilderStage, PushOptions, + RemoteCallbacks, +}; use scopetime::scope_time; +/// +#[derive(Debug, Clone, Copy)] +pub enum ProgressNotification { + /// + PushTransfer { + /// + current: usize, + /// + total: usize, + /// + bytes: usize, + }, + /// + Packing { + /// + stage: PackBuilderStage, + /// + total: usize, + /// + current: usize, + }, + /// + Done, +} + /// pub fn get_remotes(repo_path: &str) -> Result> { scope_time!("get_remotes"); @@ -24,7 +53,7 @@ pub fn fetch_origin(repo_path: &str, branch: &str) -> Result { let mut remote = repo.find_remote("origin")?; let mut options = FetchOptions::new(); - options.remote_callbacks(remote_callbacks()); + options.remote_callbacks(remote_callbacks(None)); remote.fetch(&[branch], Some(&mut options), None)?; @@ -32,10 +61,11 @@ pub fn fetch_origin(repo_path: &str, branch: &str) -> Result { } /// -pub fn push_origin( +pub fn push( repo_path: &str, remote: &str, branch: &str, + progress_sender: Sender, ) -> Result<()> { scope_time!("push_origin"); @@ -43,7 +73,8 @@ pub fn push_origin( let mut remote = repo.find_remote(remote)?; let mut options = PushOptions::new(); - options.remote_callbacks(remote_callbacks()); + + options.remote_callbacks(remote_callbacks(Some(progress_sender))); options.packbuilder_parallelism(0); remote.push(&[branch], Some(&mut options))?; @@ -51,18 +82,37 @@ pub fn push_origin( Ok(()) } -fn remote_callbacks<'a>() -> RemoteCallbacks<'a> { +fn remote_callbacks<'a>( + sender: Option>, +) -> RemoteCallbacks<'a> { let mut callbacks = RemoteCallbacks::new(); - callbacks.push_transfer_progress(|progress, total, bytes| { - log::debug!( - "progress: {}/{} ({} B)", - progress, - total, - bytes, - ); + let sender_clone = sender.clone(); + callbacks.push_transfer_progress(move |current, total, bytes| { + sender_clone.clone().map(|sender| { + sender.send(ProgressNotification::PushTransfer { + current, + total, + bytes, + }) + }); + + // log::debug!( + // "progress: {}/{} ({} B)", + // progress, + // total, + // bytes, + // ); }); - callbacks.pack_progress(|stage, current, total| { - log::debug!("packing: {:?} - {}/{}", stage, current, total); + callbacks.pack_progress(move |stage, current, total| { + sender.clone().map(|sender| { + sender.send(ProgressNotification::Packing { + stage, + total, + current, + }) + }); + + // log::debug!("packing: {:?} - {}/{}", stage, current, total); }); callbacks.credentials(|url, username_from_url, allowed_types| { log::debug!( diff --git a/src/components/push.rs b/src/components/push.rs index 91aca8cf..8cff069a 100644 --- a/src/components/push.rs +++ b/src/components/push.rs @@ -9,13 +9,17 @@ use crate::{ ui::{self, style::SharedTheme}, }; use anyhow::Result; -use asyncgit::{AsyncNotification, AsyncPush, PushRequest}; +use asyncgit::{ + AsyncNotification, AsyncPush, PushProgress, PushProgressState, + PushRequest, +}; use crossbeam_channel::Sender; use crossterm::event::Event; use tui::{ backend::Backend, layout::Rect, - widgets::{Block, BorderType, Borders, Clear, Paragraph, Text}, + style::{Color, Style}, + widgets::{Block, BorderType, Borders, Clear, Gauge}, Frame, }; @@ -23,6 +27,7 @@ use tui::{ pub struct PushComponent { visible: bool, git_push: AsyncPush, + progress: Option, pending: bool, queue: Queue, theme: SharedTheme, @@ -42,6 +47,7 @@ impl PushComponent { pending: false, visible: false, git_push: AsyncPush::new(sender), + progress: None, theme, key_config, } @@ -50,6 +56,7 @@ impl PushComponent { /// pub fn push(&mut self, branch: String) -> Result<()> { self.pending = true; + self.progress = None; self.git_push.request(PushRequest { remote: String::from("origin"), branch, @@ -75,6 +82,7 @@ impl PushComponent { /// fn update(&mut self) -> Result<()> { self.pending = self.git_push.is_pending()?; + self.progress = self.git_push.progress()?; if !self.pending { if let Some(err) = self.git_push.last_result()? { @@ -91,6 +99,33 @@ impl PushComponent { Ok(()) } + + fn get_progress(&self) -> (String, u8) { + self.progress.as_ref().map_or( + (strings::PUSH_POPUP_PROGRESS_NONE.into(), 0), + |progress| { + ( + Self::progress_state_name(&progress.state), + progress.progress, + ) + }, + ) + } + + fn progress_state_name(state: &PushProgressState) -> String { + match state { + PushProgressState::PackingAddingObject => { + strings::PUSH_POPUP_STATES_ADDING + } + PushProgressState::PackingDeltafiction => { + strings::PUSH_POPUP_STATES_DELTAS + } + PushProgressState::Pushing => { + strings::PUSH_POPUP_STATES_PUSHING + } + } + .into() + } } impl DrawableComponent for PushComponent { @@ -100,20 +135,28 @@ impl DrawableComponent for PushComponent { _rect: Rect, ) -> Result<()> { if self.visible { - let txt = vec![Text::Raw(strings::PUSH_POPUP_MSG.into())]; + let (state, progress) = self.get_progress(); + + let area = ui::centered_rect_absolute(30, 3, f.size()); - let area = ui::centered_rect_absolute(25, 3, f.size()); f.render_widget(Clear, area); f.render_widget( - Paragraph::new(txt.iter()) + Gauge::default() + .label(state.as_str()) .block( Block::default() + .title(strings::PUSH_POPUP_MSG) .borders(Borders::ALL) .border_type(BorderType::Thick) .title_style(self.theme.title(true)) .border_style(self.theme.block(true)), ) - .style(self.theme.text_danger()), + .style( + Style::default() + .fg(Color::White) + .bg(Color::Black), // .modifier(Modifier::ITALIC), + ) + .percent(u16::from(progress)), area, ); } diff --git a/src/strings.rs b/src/strings.rs index 42486011..bbedebe8 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -4,7 +4,11 @@ pub mod order { pub static NAV: i8 = 1; } -pub static PUSH_POPUP_MSG: &str = "pushing..."; +pub static PUSH_POPUP_MSG: &str = "Push"; +pub static PUSH_POPUP_PROGRESS_NONE: &str = "preparing..."; +pub static PUSH_POPUP_STATES_ADDING: &str = "adding objects (1/3)"; +pub static PUSH_POPUP_STATES_DELTAS: &str = "deltas (2/3)"; +pub static PUSH_POPUP_STATES_PUSHING: &str = "pushing (3/3)"; pub fn title_status(key_config: &SharedKeyConfig) -> String { format!(