Push progress

closes #267
This commit is contained in:
Stephan Dilly 2020-09-06 17:48:14 +02:00 committed by GitHub
parent 17de5a900c
commit de80601bf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 300 additions and 39 deletions

View File

@ -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))

View File

@ -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

BIN
assets/push.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

View File

@ -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::{

View File

@ -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<ProgressNotification> 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<Mutex<Option<PushState>>>,
last_result: Arc<Mutex<Option<String>>>,
progress: Arc<Mutex<Option<ProgressNotification>>>,
sender: Sender<AsyncNotification>,
}
@ -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<Option<PushProgress>> {
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(&params)?;
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<AsyncNotification>,
receiver: Receiver<ProgressNotification>,
progress: Arc<Mutex<Option<ProgressNotification>>>,
) -> 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<Mutex<Option<ProgressNotification>>>,
state: Option<ProgressNotification>,
) -> Result<()> {
let simple_progress: Option<PushProgress> =
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<Mutex<Option<String>>>,
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);
}
}

View File

@ -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};

View File

@ -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<Vec<String>> {
scope_time!("get_remotes");
@ -24,7 +53,7 @@ pub fn fetch_origin(repo_path: &str, branch: &str) -> Result<usize> {
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<usize> {
}
///
pub fn push_origin(
pub fn push(
repo_path: &str,
remote: &str,
branch: &str,
progress_sender: Sender<ProgressNotification>,
) -> 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<Sender<ProgressNotification>>,
) -> 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!(

View File

@ -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<PushProgress>,
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,
);
}

View File

@ -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!(