From f1fcd341c62344d73988c9900031317a9d09d063 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Fri, 5 Mar 2021 17:49:40 +0100 Subject: [PATCH] add support for pushing tags (#569) --- .clippy.toml | 3 +- CHANGELOG.md | 3 + asyncgit/src/lib.rs | 5 + asyncgit/src/progress.rs | 47 +++++ asyncgit/src/push.rs | 2 +- asyncgit/src/push_tags.rs | 153 +++++++++++++++ asyncgit/src/remote_progress.rs | 66 +++---- asyncgit/src/sync/mod.rs | 5 +- asyncgit/src/sync/remotes/mod.rs | 7 +- asyncgit/src/sync/remotes/push.rs | 47 ++++- asyncgit/src/sync/remotes/tags.rs | 303 ++++++++++++++++++++++++++++++ src/app.rs | 21 ++- src/components/mod.rs | 2 + src/components/push.rs | 2 +- src/components/push_tags.rs | 263 ++++++++++++++++++++++++++ src/queue.rs | 2 + src/strings.rs | 15 ++ src/tabs/revlog.rs | 11 ++ 18 files changed, 903 insertions(+), 54 deletions(-) create mode 100644 asyncgit/src/progress.rs create mode 100644 asyncgit/src/push_tags.rs create mode 100644 asyncgit/src/sync/remotes/tags.rs create mode 100644 src/components/push_tags.rs diff --git a/.clippy.toml b/.clippy.toml index 87f21c64..940e1282 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,3 +1,2 @@ msrv = "1.50.0" -cognitive-complexity-threshold = 18 -too-many-lines-threshold = 105 \ No newline at end of file +cognitive-complexity-threshold = 18 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f0dc3a25..b44b697d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- support for pushing tags ([#568](https://github.com/extrawurst/gitui/issues/568)) + ## [0.12.0] - 2020-03-03 **pull support (ff-merge or conflict-free merge-commit)** diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index 4dc3b01f..f17824bb 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -15,7 +15,9 @@ mod commit_files; mod diff; mod error; mod fetch; +mod progress; mod push; +mod push_tags; pub mod remote_progress; mod revlog; mod status; @@ -27,6 +29,7 @@ pub use crate::{ diff::{AsyncDiff, DiffParams, DiffType}, fetch::{AsyncFetch, FetchRequest}, push::{AsyncPush, PushRequest}, + push_tags::{AsyncPushTags, PushTagsRequest}, remote_progress::{RemoteProgress, RemoteProgressState}, revlog::{AsyncLog, FetchStatus}, status::{AsyncStatus, StatusParams}, @@ -59,6 +62,8 @@ pub enum AsyncNotification { /// Push, /// + PushTags, + /// Fetch, } diff --git a/asyncgit/src/progress.rs b/asyncgit/src/progress.rs new file mode 100644 index 00000000..19680c26 --- /dev/null +++ b/asyncgit/src/progress.rs @@ -0,0 +1,47 @@ +//! + +use std::cmp; + +/// +#[derive(Clone, Debug)] +pub struct ProgressPercent { + /// percent 0..100 + pub progress: u8, +} + +impl ProgressPercent { + /// + pub fn new(current: usize, total: usize) -> Self { + let total = cmp::max(current, total) as f32; + let progress = current as f32 / total * 100.0; + let progress = progress as u8; + Self { progress } + } + /// + pub fn empty() -> Self { + Self { progress: 0 } + } + /// + pub fn full() -> Self { + Self { progress: 100 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_progress_zero_total() { + let prog = ProgressPercent::new(1, 0); + + assert_eq!(prog.progress, 100); + } + + #[test] + fn test_progress_rounding() { + let prog = ProgressPercent::new(2, 10); + + assert_eq!(prog.progress, 20); + } +} diff --git a/asyncgit/src/push.rs b/asyncgit/src/push.rs index bd027d79..02c56f83 100644 --- a/asyncgit/src/push.rs +++ b/asyncgit/src/push.rs @@ -98,7 +98,7 @@ impl AsyncPush { params.remote.as_str(), params.branch.as_str(), params.force, - params.basic_credential, + params.basic_credential.clone(), Some(progress_sender.clone()), ); diff --git a/asyncgit/src/push_tags.rs b/asyncgit/src/push_tags.rs new file mode 100644 index 00000000..cfbe5a73 --- /dev/null +++ b/asyncgit/src/push_tags.rs @@ -0,0 +1,153 @@ +use crate::{ + error::{Error, Result}, + sync::{ + cred::BasicAuthCredential, + remotes::tags::{push_tags, PushTagsProgress}, + }, + AsyncNotification, RemoteProgress, CWD, +}; +use crossbeam_channel::{unbounded, Sender}; +use std::{ + sync::{Arc, Mutex}, + thread, +}; + +/// +#[derive(Default, Clone, Debug)] +pub struct PushTagsRequest { + /// + pub remote: String, + /// + pub basic_credential: Option, +} + +#[derive(Default, Clone, Debug)] +struct PushState { + request: PushTagsRequest, +} + +/// +pub struct AsyncPushTags { + state: Arc>>, + last_result: Arc>>, + progress: Arc>>, + sender: Sender, +} + +impl AsyncPushTags { + /// + pub fn new(sender: &Sender) -> Self { + Self { + state: Arc::new(Mutex::new(None)), + last_result: Arc::new(Mutex::new(None)), + progress: Arc::new(Mutex::new(None)), + sender: sender.clone(), + } + } + + /// + pub fn is_pending(&self) -> Result { + let state = self.state.lock()?; + Ok(state.is_some()) + } + + /// + pub fn last_result(&self) -> Result> { + let res = self.last_result.lock()?; + Ok(res.clone()) + } + + /// + pub fn progress(&self) -> Result> { + let res = self.progress.lock()?; + Ok(*res) + } + + /// + pub fn request(&mut self, params: PushTagsRequest) -> Result<()> { + log::trace!("request"); + + if self.is_pending()? { + return Ok(()); + } + + self.set_request(¶ms)?; + RemoteProgress::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(); + + thread::spawn(move || { + let (progress_sender, receiver) = unbounded(); + + let handle = RemoteProgress::spawn_receiver_thread( + AsyncNotification::PushTags, + sender.clone(), + receiver, + arc_progress, + ); + + let res = push_tags( + CWD, + params.remote.as_str(), + params.basic_credential.clone(), + Some(progress_sender), + ); + + handle.join().expect("joining thread failed"); + + Self::set_result(arc_res, res).expect("result error"); + + Self::clear_request(arc_state).expect("clear error"); + + sender + .send(AsyncNotification::PushTags) + .expect("error sending push"); + }); + + Ok(()) + } + + fn set_request(&self, params: &PushTagsRequest) -> Result<()> { + let mut state = self.state.lock()?; + + if state.is_some() { + return Err(Error::Generic("pending request".into())); + } + + *state = Some(PushState { + request: params.clone(), + }); + + Ok(()) + } + + fn clear_request( + state: Arc>>, + ) -> Result<()> { + let mut state = state.lock()?; + + *state = None; + + Ok(()) + } + + fn set_result( + arc_result: Arc>>, + res: Result<()>, + ) -> Result<()> { + let mut last_res = arc_result.lock()?; + + *last_res = match res { + Ok(_) => None, + Err(e) => { + log::error!("push error: {}", e); + Some(e.to_string()) + } + }; + + Ok(()) + } +} diff --git a/asyncgit/src/remote_progress.rs b/asyncgit/src/remote_progress.rs index 1d1cd27d..8df50bca 100644 --- a/asyncgit/src/remote_progress.rs +++ b/asyncgit/src/remote_progress.rs @@ -1,19 +1,20 @@ //! use crate::{ - error::Result, sync::remotes::push::ProgressNotification, + error::Result, + progress::ProgressPercent, + sync::remotes::push::{AsyncProgress, ProgressNotification}, AsyncNotification, }; use crossbeam_channel::{Receiver, Sender}; use git2::PackBuilderStage; use std::{ - cmp, sync::{Arc, Mutex}, thread::{self, JoinHandle}, time::Duration, }; -/// +/// used for push/pull #[derive(Clone, Debug)] pub enum RemoteProgressState { /// @@ -33,8 +34,8 @@ pub enum RemoteProgressState { pub struct RemoteProgress { /// pub state: RemoteProgressState, - /// percent 0..100 - pub progress: u8, + /// + pub progress: ProgressPercent, } impl RemoteProgress { @@ -44,15 +45,20 @@ impl RemoteProgress { current: usize, total: usize, ) -> Self { - let total = cmp::max(current, total) as f32; - let progress = current as f32 / total * 100.0; - let progress = progress as u8; - Self { state, progress } + Self { + state, + progress: ProgressPercent::new(current, total), + } } - pub(crate) fn set_progress( - progress: Arc>>, - state: Option, + /// + pub fn get_progress_percent(&self) -> u8 { + self.progress.progress + } + + pub(crate) fn set_progress( + progress: Arc>>, + state: Option, ) -> Result<()> { let mut progress = progress.lock()?; @@ -62,11 +68,13 @@ impl RemoteProgress { } /// spawn thread to listen to progress notifcations coming in from blocking remote git method (fetch/push) - pub(crate) fn spawn_receiver_thread( + pub(crate) fn spawn_receiver_thread< + T: 'static + AsyncProgress, + >( notification_type: AsyncNotification, sender: Sender, - receiver: Receiver, - progress: Arc>>, + receiver: Receiver, + progress: Arc>>, ) -> JoinHandle<()> { thread::spawn(move || loop { let incoming = receiver.recv(); @@ -76,7 +84,7 @@ impl RemoteProgress { progress.clone(), Some(update.clone()), ) - .expect("set prgoress failed"); + .expect("set progress failed"); sender .send(notification_type) .expect("Notification error"); @@ -84,13 +92,13 @@ impl RemoteProgress { //NOTE: for better debugging thread::sleep(Duration::from_millis(1)); - if let ProgressNotification::Done = update { + if update.is_done() { break; } } Err(e) => { log::error!( - "push progress receiver error: {}", + "remote progress receiver error: {}", e ); break; @@ -145,25 +153,3 @@ impl From for RemoteProgress { } } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::remote_progress::RemoteProgressState; - - #[test] - fn test_progress_zero_total() { - let prog = - RemoteProgress::new(RemoteProgressState::Pushing, 1, 0); - - assert_eq!(prog.progress, 100); - } - - #[test] - fn test_progress_rounding() { - let prog = - RemoteProgress::new(RemoteProgressState::Pushing, 2, 10); - - assert_eq!(prog.progress, 20); - } -} diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index f43e0d6f..2d96d265 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -42,7 +42,10 @@ pub use hooks::{ pub use hunks::{reset_hunk, stage_hunk, unstage_hunk}; pub use ignore::add_to_ignore; pub use logwalker::LogWalker; -pub use remotes::{get_default_remote, get_remotes}; +pub use remotes::{ + get_default_remote, get_remotes, push::AsyncProgress, + tags::PushTagsProgress, +}; pub use reset::{reset_stage, reset_workdir}; pub use stash::{get_stashes, stash_apply, stash_drop, stash_save}; pub use state::{repo_state, RepoState}; diff --git a/asyncgit/src/sync/remotes/mod.rs b/asyncgit/src/sync/remotes/mod.rs index 6143b32c..e8a6ec13 100644 --- a/asyncgit/src/sync/remotes/mod.rs +++ b/asyncgit/src/sync/remotes/mod.rs @@ -1,7 +1,10 @@ //! pub(crate) mod push; +pub(crate) mod tags; +use self::push::ProgressNotification; +use super::cred::BasicAuthCredential; use crate::{ error::{Error, Result}, sync::utils, @@ -11,10 +14,6 @@ use git2::{FetchOptions, Repository}; use push::remote_callbacks; use scopetime::scope_time; -use self::push::ProgressNotification; - -use super::cred::BasicAuthCredential; - /// origin pub const DEFAULT_REMOTE_NAME: &str = "origin"; diff --git a/asyncgit/src/sync/remotes/push.rs b/asyncgit/src/sync/remotes/push.rs index efefe81b..a7672028 100644 --- a/asyncgit/src/sync/remotes/push.rs +++ b/asyncgit/src/sync/remotes/push.rs @@ -1,6 +1,7 @@ use super::utils; use crate::{ error::Result, + progress::ProgressPercent, sync::{ branch::branch_set_upstream, cred::BasicAuthCredential, CommitId, @@ -14,7 +15,15 @@ use git2::{ use scopetime::scope_time; /// -#[derive(Debug, Clone)] +pub trait AsyncProgress: Clone + Send + Sync { + /// + fn is_done(&self) -> bool; + /// + fn progress(&self) -> ProgressPercent; +} + +/// +#[derive(Debug, Clone, PartialEq)] pub(crate) enum ProgressNotification { /// UpdateTips { @@ -54,6 +63,39 @@ pub(crate) enum ProgressNotification { Done, } +impl AsyncProgress for ProgressNotification { + fn is_done(&self) -> bool { + *self == ProgressNotification::Done + } + fn progress(&self) -> ProgressPercent { + match *self { + ProgressNotification::Packing { + stage, + current, + total, + } => match stage { + PackBuilderStage::AddingObjects => { + ProgressPercent::new(current, total) + } + PackBuilderStage::Deltafication => { + ProgressPercent::new(current, total) + } + }, + ProgressNotification::PushTransfer { + current, + total, + .. + } => ProgressPercent::new(current, total), + ProgressNotification::Transfer { + objects, + total_objects, + .. + } => ProgressPercent::new(objects, total_objects), + _ => ProgressPercent::full(), + } + } +} + /// pub(crate) fn push( repo_path: &str, @@ -207,13 +249,12 @@ pub(crate) fn remote_callbacks<'a>( #[cfg(test)] mod tests { - use git2::Repository; - use super::*; use crate::sync::{ self, tests::{get_commit_ids, repo_init, repo_init_bare}, }; + use git2::Repository; use std::{fs::File, io::Write, path::Path}; #[test] diff --git a/asyncgit/src/sync/remotes/tags.rs b/asyncgit/src/sync/remotes/tags.rs new file mode 100644 index 00000000..e30021a6 --- /dev/null +++ b/asyncgit/src/sync/remotes/tags.rs @@ -0,0 +1,303 @@ +//! + +use std::collections::HashSet; + +use super::{ + push::{remote_callbacks, AsyncProgress}, + utils, +}; +use crate::{ + error::Result, progress::ProgressPercent, + sync::cred::BasicAuthCredential, +}; +use crossbeam_channel::Sender; +use git2::{Direction, PushOptions}; +use scopetime::scope_time; + +/// +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum PushTagsProgress { + /// fetching tags from remote to check which local tags need pushing + CheckRemote, + /// pushing local tags that are missing remote + Push { + /// + pushed: usize, + /// + total: usize, + }, + /// done + Done, +} + +impl AsyncProgress for PushTagsProgress { + fn progress(&self) -> ProgressPercent { + match self { + PushTagsProgress::CheckRemote => ProgressPercent::empty(), + PushTagsProgress::Push { pushed, total } => { + ProgressPercent::new(*pushed, *total) + } + PushTagsProgress::Done => ProgressPercent::full(), + } + } + fn is_done(&self) -> bool { + *self == PushTagsProgress::Done + } +} + +/// lists the remotes tags +fn remote_tag_refs( + repo_path: &str, + remote: &str, + basic_credential: Option, +) -> Result> { + scope_time!("remote_tags"); + + let repo = utils::repo(repo_path)?; + let mut remote = repo.find_remote(remote)?; + let conn = remote.connect_auth( + Direction::Fetch, + Some(remote_callbacks(None, basic_credential)), + None, + )?; + + let remote_heads = conn.list()?; + let remote_tags = remote_heads + .iter() + .map(|s| s.name().to_string()) + .filter(|name| { + name.starts_with("refs/tags/") && !name.ends_with("^{}") + }) + .collect::>(); + + Ok(remote_tags) +} + +/// lists the remotes tags missing +fn tags_missing_remote( + repo_path: &str, + remote: &str, + basic_credential: Option, +) -> Result> { + scope_time!("tags_missing_remote"); + + let repo = utils::repo(repo_path)?; + let tags = repo.tag_names(None)?; + + let mut local_tags = tags + .iter() + .filter_map(|tag| tag.map(|tag| format!("refs/tags/{}", tag))) + .collect::>(); + let remote_tags = + remote_tag_refs(repo_path, remote, basic_credential)?; + + for t in remote_tags { + local_tags.remove(&t); + } + + Ok(local_tags.into_iter().collect()) +} + +/// +pub(crate) fn push_tags( + repo_path: &str, + remote: &str, + basic_credential: Option, + progress_sender: Option>, +) -> Result<()> { + scope_time!("push_tags"); + + progress_sender + .as_ref() + .map(|sender| sender.send(PushTagsProgress::CheckRemote)); + + let tags_missing = tags_missing_remote( + repo_path, + remote, + basic_credential.clone(), + )?; + + let repo = utils::repo(repo_path)?; + let mut remote = repo.find_remote(remote)?; + + let total = tags_missing.len(); + + progress_sender.as_ref().map(|sender| { + sender.send(PushTagsProgress::Push { pushed: 0, total }) + }); + + for (idx, tag) in tags_missing.into_iter().enumerate() { + let mut options = PushOptions::new(); + options.remote_callbacks(remote_callbacks( + None, + basic_credential.clone(), + )); + options.packbuilder_parallelism(0); + remote.push(&[tag.as_str()], Some(&mut options))?; + + progress_sender.as_ref().map(|sender| { + sender.send(PushTagsProgress::Push { + pushed: idx + 1, + total, + }) + }); + } + + progress_sender + .as_ref() + .map(|sender| sender.send(PushTagsProgress::Done)); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sync::{ + self, + remotes::{fetch_origin, push::push}, + tests::{repo_clone, repo_init_bare}, + CommitId, + }; + use git2::Repository; + use std::{fs::File, io::Write, path::Path}; + + // write, stage and commit a file + fn write_commit_file( + repo: &Repository, + file: &str, + content: &str, + commit_name: &str, + ) -> CommitId { + File::create( + repo.workdir().unwrap().join(file).to_str().unwrap(), + ) + .unwrap() + .write_all(content.as_bytes()) + .unwrap(); + + sync::stage_add_file( + repo.workdir().unwrap().to_str().unwrap(), + Path::new(file), + ) + .unwrap(); + + sync::commit( + repo.workdir().unwrap().to_str().unwrap(), + commit_name, + ) + .unwrap() + } + + #[test] + fn test_push_pull_tags() { + let (r1_dir, _repo) = repo_init_bare().unwrap(); + let r1_dir = r1_dir.path().to_str().unwrap(); + + let (clone1_dir, clone1) = repo_clone(r1_dir).unwrap(); + + let clone1_dir = clone1_dir.path().to_str().unwrap(); + + let (clone2_dir, clone2) = repo_clone(r1_dir).unwrap(); + + let clone2_dir = clone2_dir.path().to_str().unwrap(); + + // clone1 + + let commit1 = + write_commit_file(&clone1, "test.txt", "test", "commit1"); + + sync::tag(clone1_dir, &commit1, "tag1").unwrap(); + + push(clone1_dir, "origin", "master", false, None, None) + .unwrap(); + push_tags(clone1_dir, "origin", None, None).unwrap(); + + // clone2 + + let _commit2 = write_commit_file( + &clone2, + "test2.txt", + "test", + "commit2", + ); + + assert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 0); + + //lets fetch from origin + let bytes = + fetch_origin(clone2_dir, "master", None, None).unwrap(); + assert!(bytes > 0); + + sync::merge_upstream_commit(clone2_dir, "master").unwrap(); + + assert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 1); + } + + #[test] + fn test_get_remote_tags() { + let (r1_dir, _repo) = repo_init_bare().unwrap(); + let r1_dir = r1_dir.path().to_str().unwrap(); + + let (clone1_dir, clone1) = repo_clone(r1_dir).unwrap(); + + let clone1_dir = clone1_dir.path().to_str().unwrap(); + + let (clone2_dir, _clone2) = repo_clone(r1_dir).unwrap(); + + let clone2_dir = clone2_dir.path().to_str().unwrap(); + + // clone1 + + let commit1 = + write_commit_file(&clone1, "test.txt", "test", "commit1"); + + sync::tag(clone1_dir, &commit1, "tag1").unwrap(); + + push(clone1_dir, "origin", "master", false, None, None) + .unwrap(); + push_tags(clone1_dir, "origin", None, None).unwrap(); + + // clone2 + + let tags = + remote_tag_refs(clone2_dir, "origin", None).unwrap(); + + assert_eq!( + tags.as_slice(), + &[String::from("refs/tags/tag1")] + ); + } + + #[test] + fn test_tags_missing_remote() { + let (r1_dir, _repo) = repo_init_bare().unwrap(); + let r1_dir = r1_dir.path().to_str().unwrap(); + + let (clone1_dir, clone1) = repo_clone(r1_dir).unwrap(); + + let clone1_dir = clone1_dir.path().to_str().unwrap(); + + // clone1 + + let commit1 = + write_commit_file(&clone1, "test.txt", "test", "commit1"); + + sync::tag(clone1_dir, &commit1, "tag1").unwrap(); + + push(clone1_dir, "origin", "master", false, None, None) + .unwrap(); + + let tags_missing = + tags_missing_remote(clone1_dir, "origin", None).unwrap(); + + assert_eq!( + tags_missing.as_slice(), + &[String::from("refs/tags/tag1")] + ); + push_tags(clone1_dir, "origin", None, None).unwrap(); + let tags_missing = + tags_missing_remote(clone1_dir, "origin", None).unwrap(); + assert!(tags_missing.is_empty()); + } +} diff --git a/src/app.rs b/src/app.rs index c7b305e3..30e9dab3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,8 +7,8 @@ use crate::{ CreateBranchComponent, DrawableComponent, ExternalEditorComponent, HelpComponent, InspectCommitComponent, MsgComponent, PullComponent, - PushComponent, RenameBranchComponent, ResetComponent, - StashMsgComponent, TagCommitComponent, + PushComponent, PushTagsComponent, RenameBranchComponent, + ResetComponent, StashMsgComponent, TagCommitComponent, }, input::{Input, InputEvent, InputState}, keys::{KeyConfig, SharedKeyConfig}, @@ -45,6 +45,7 @@ pub struct App { inspect_commit_popup: InspectCommitComponent, external_editor_popup: ExternalEditorComponent, push_popup: PushComponent, + push_tags_popup: PushTagsComponent, pull_popup: PullComponent, tag_commit_popup: TagCommitComponent, create_branch_popup: CreateBranchComponent, @@ -69,6 +70,7 @@ pub struct App { // public interface impl App { /// + #[allow(clippy::too_many_lines)] pub fn new( sender: &Sender, input: Input, @@ -111,6 +113,12 @@ impl App { theme.clone(), key_config.clone(), ), + push_tags_popup: PushTagsComponent::new( + &queue, + sender, + theme.clone(), + key_config.clone(), + ), pull_popup: PullComponent::new( &queue, sender, @@ -308,6 +316,7 @@ impl App { self.revlog.update_git(ev)?; self.inspect_commit_popup.update_git(ev)?; self.push_popup.update_git(ev)?; + self.push_tags_popup.update_git(ev)?; self.pull_popup.update_git(ev)?; //TODO: better system for this @@ -330,6 +339,7 @@ impl App { || self.inspect_commit_popup.any_work_pending() || self.input.is_state_changing() || self.push_popup.any_work_pending() + || self.push_tags_popup.any_work_pending() || self.pull_popup.any_work_pending() } @@ -356,6 +366,7 @@ impl App { inspect_commit_popup, external_editor_popup, push_popup, + push_tags_popup, pull_popup, tag_commit_popup, create_branch_popup, @@ -561,6 +572,10 @@ impl App { self.pull_popup.fetch(branch)?; flags.insert(NeedsUpdate::ALL) } + InternalEvent::PushTags => { + self.push_tags_popup.push_tags()?; + flags.insert(NeedsUpdate::ALL) + } }; Ok(flags) @@ -621,6 +636,7 @@ impl App { || self.tag_commit_popup.is_visible() || self.create_branch_popup.is_visible() || self.push_popup.is_visible() + || self.push_tags_popup.is_visible() || self.pull_popup.is_visible() || self.select_branch_popup.is_visible() || self.rename_branch_popup.is_visible() @@ -651,6 +667,7 @@ impl App { self.create_branch_popup.draw(f, size)?; self.rename_branch_popup.draw(f, size)?; self.push_popup.draw(f, size)?; + self.push_tags_popup.draw(f, size)?; self.pull_popup.draw(f, size)?; self.reset.draw(f, size)?; self.msg.draw(f, size)?; diff --git a/src/components/mod.rs b/src/components/mod.rs index 3408103c..3336d718 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -14,6 +14,7 @@ mod inspect_commit; mod msg; mod pull; mod push; +mod push_tags; mod rename_branch; mod reset; mod stashmsg; @@ -36,6 +37,7 @@ pub use inspect_commit::InspectCommitComponent; pub use msg::MsgComponent; pub use pull::PullComponent; pub use push::PushComponent; +pub use push_tags::PushTagsComponent; pub use rename_branch::RenameBranchComponent; pub use reset::ResetComponent; pub use stashmsg::StashMsgComponent; diff --git a/src/components/push.rs b/src/components/push.rs index 168d8f15..5fe77de5 100644 --- a/src/components/push.rs +++ b/src/components/push.rs @@ -158,7 +158,7 @@ impl PushComponent { |progress| { ( Self::progress_state_name(&progress.state), - progress.progress, + progress.get_progress_percent(), ) }, ) diff --git a/src/components/push_tags.rs b/src/components/push_tags.rs new file mode 100644 index 00000000..cde8fd74 --- /dev/null +++ b/src/components/push_tags.rs @@ -0,0 +1,263 @@ +use crate::{ + components::{ + cred::CredComponent, visibility_blocking, CommandBlocking, + CommandInfo, Component, DrawableComponent, + }, + keys::SharedKeyConfig, + queue::{InternalEvent, Queue}, + strings::{self}, + ui::{self, style::SharedTheme}, +}; +use anyhow::Result; +use asyncgit::{ + sync::{ + cred::{ + extract_username_password, need_username_password, + BasicAuthCredential, + }, + get_default_remote, AsyncProgress, PushTagsProgress, + }, + AsyncNotification, AsyncPushTags, PushTagsRequest, CWD, +}; +use crossbeam_channel::Sender; +use crossterm::event::Event; +use tui::{ + backend::Backend, + layout::Rect, + text::Span, + widgets::{Block, BorderType, Borders, Clear, Gauge}, + Frame, +}; + +/// +pub struct PushTagsComponent { + visible: bool, + git_push: AsyncPushTags, + progress: Option, + pending: bool, + queue: Queue, + theme: SharedTheme, + key_config: SharedKeyConfig, + input_cred: CredComponent, +} + +impl PushTagsComponent { + /// + pub fn new( + queue: &Queue, + sender: &Sender, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + queue: queue.clone(), + pending: false, + visible: false, + git_push: AsyncPushTags::new(sender), + progress: None, + input_cred: CredComponent::new( + theme.clone(), + key_config.clone(), + ), + theme, + key_config, + } + } + + /// + pub fn push_tags(&mut self) -> Result<()> { + self.show()?; + if need_username_password()? { + let cred = + extract_username_password().unwrap_or_else(|_| { + BasicAuthCredential::new(None, None) + }); + if cred.is_complete() { + self.push_to_remote(Some(cred)) + } else { + self.input_cred.set_cred(cred); + self.input_cred.show() + } + } else { + self.push_to_remote(None) + } + } + + fn push_to_remote( + &mut self, + cred: Option, + ) -> Result<()> { + self.pending = true; + self.progress = None; + self.git_push.request(PushTagsRequest { + remote: get_default_remote(CWD)?, + basic_credential: cred, + })?; + Ok(()) + } + + /// + pub fn update_git( + &mut self, + ev: AsyncNotification, + ) -> Result<()> { + if self.is_visible() { + if let AsyncNotification::PushTags = ev { + self.update()?; + } + } + + Ok(()) + } + + /// + 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()? { + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg(format!( + "push tags failed:\n{}", + err + )), + ); + } + self.hide(); + } + + Ok(()) + } + + /// + pub const fn any_work_pending(&self) -> bool { + self.pending + } + + /// + pub fn get_progress( + progress: &Option, + ) -> (String, u8) { + progress.as_ref().map_or( + (strings::PUSH_POPUP_PROGRESS_NONE.into(), 0), + |progress| { + ( + Self::progress_state_name(progress), + progress.progress().progress, + ) + }, + ) + } + + fn progress_state_name(progress: &PushTagsProgress) -> String { + match progress { + PushTagsProgress::CheckRemote => { + strings::PUSH_TAGS_STATES_FETCHING + } + PushTagsProgress::Push { .. } => { + strings::PUSH_TAGS_STATES_PUSHING + } + PushTagsProgress::Done => strings::PUSH_TAGS_STATES_DONE, + } + .to_string() + } +} + +impl DrawableComponent for PushTagsComponent { + fn draw( + &self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + if self.visible { + let (state, progress) = + Self::get_progress(&self.progress); + + let area = ui::centered_rect_absolute(30, 3, f.size()); + + f.render_widget(Clear, area); + f.render_widget( + Gauge::default() + .label(state.as_str()) + .block( + Block::default() + .title(Span::styled( + strings::PUSH_TAGS_POPUP_MSG, + self.theme.title(true), + )) + .borders(Borders::ALL) + .border_type(BorderType::Thick) + .border_style(self.theme.block(true)), + ) + .gauge_style(self.theme.push_gauge()) + .percent(u16::from(progress)), + area, + ); + self.input_cred.draw(f, rect)?; + } + + Ok(()) + } +} + +impl Component for PushTagsComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() { + out.clear(); + } + + if self.input_cred.is_visible() { + self.input_cred.commands(out, force_all) + } else { + out.push(CommandInfo::new( + strings::commands::close_msg(&self.key_config), + !self.pending, + self.visible, + )); + visibility_blocking(self) + } + } + + fn event(&mut self, ev: Event) -> Result { + if self.visible { + if let Event::Key(e) = ev { + if self.input_cred.is_visible() { + if self.input_cred.event(ev)? { + return Ok(true); + } else if self.input_cred.get_cred().is_complete() + { + self.push_to_remote(Some( + self.input_cred.get_cred().clone(), + ))?; + self.input_cred.hide(); + } + } else if e == self.key_config.exit_popup + && !self.pending + { + self.hide(); + } + } + return Ok(true); + } + Ok(false) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } +} diff --git a/src/queue.rs b/src/queue.rs index da2e480b..fbafee7e 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -65,6 +65,8 @@ pub enum InternalEvent { Push(String, bool), /// Pull(String), + /// + PushTags, } /// diff --git a/src/strings.rs b/src/strings.rs index 87ad8cc7..0eff7ae2 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -14,6 +14,11 @@ pub static PUSH_POPUP_STATES_PUSHING: &str = "pushing (3/3)"; pub static PUSH_POPUP_STATES_TRANSFER: &str = "transfer"; pub static PUSH_POPUP_STATES_DONE: &str = "done"; +pub static PUSH_TAGS_POPUP_MSG: &str = "Push Tags"; +pub static PUSH_TAGS_STATES_FETCHING: &str = "fetching"; +pub static PUSH_TAGS_STATES_PUSHING: &str = "pushing"; +pub static PUSH_TAGS_STATES_DONE: &str = "done"; + pub static SELECT_BRANCH_POPUP_MSG: &str = "Switch Branch"; pub fn title_status(key_config: &SharedKeyConfig) -> String { @@ -341,6 +346,16 @@ pub mod commands { CMD_GROUP_LOG, ) } + pub fn push_tags(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Push Tags [{}]", + key_config.get_hint(key_config.push), + ), + "push tags to remote", + CMD_GROUP_LOG, + ) + } pub fn diff_home_end( key_config: &SharedKeyConfig, ) -> CommandText { diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index 9a2a6735..d9fd3f20 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -212,6 +212,11 @@ impl Component for Revlog { } else if k == self.key_config.copy { self.copy_commit_hash()?; return Ok(true); + } else if k == self.key_config.push { + self.queue + .borrow_mut() + .push_back(InternalEvent::PushTags); + return Ok(true); } else if k == self.key_config.log_tag_commit { return self.selected_commit().map_or( Ok(false), @@ -293,6 +298,12 @@ impl Component for Revlog { self.visible || force_all, )); + out.push(CommandInfo::new( + strings::commands::push_tags(&self.key_config), + true, + self.visible || force_all, + )); + visibility_blocking(self) }