mirror of
https://github.com/extrawurst/gitui.git
synced 2024-12-27 02:53:50 +03:00
parent
17de5a900c
commit
de80601bf4
@ -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))
|
||||
|
@ -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
BIN
assets/push.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 282 KiB |
@ -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::{
|
||||
|
@ -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(¶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<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);
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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!(
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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!(
|
||||
|
Loading…
Reference in New Issue
Block a user