add support for pushing tags (#569)

This commit is contained in:
Stephan Dilly 2021-03-05 17:49:40 +01:00 committed by GitHub
parent 0d9a973e79
commit f1fcd341c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 903 additions and 54 deletions

View file

@ -1,3 +1,2 @@
msrv = "1.50.0"
cognitive-complexity-threshold = 18
too-many-lines-threshold = 105
cognitive-complexity-threshold = 18

View file

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

View file

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

47
asyncgit/src/progress.rs Normal file
View file

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

View file

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

153
asyncgit/src/push_tags.rs Normal file
View file

@ -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<BasicAuthCredential>,
}
#[derive(Default, Clone, Debug)]
struct PushState {
request: PushTagsRequest,
}
///
pub struct AsyncPushTags {
state: Arc<Mutex<Option<PushState>>>,
last_result: Arc<Mutex<Option<String>>>,
progress: Arc<Mutex<Option<PushTagsProgress>>>,
sender: Sender<AsyncNotification>,
}
impl AsyncPushTags {
///
pub fn new(sender: &Sender<AsyncNotification>) -> 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<bool> {
let state = self.state.lock()?;
Ok(state.is_some())
}
///
pub fn last_result(&self) -> Result<Option<String>> {
let res = self.last_result.lock()?;
Ok(res.clone())
}
///
pub fn progress(&self) -> Result<Option<PushTagsProgress>> {
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(&params)?;
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<Mutex<Option<PushState>>>,
) -> Result<()> {
let mut state = state.lock()?;
*state = None;
Ok(())
}
fn set_result(
arc_result: Arc<Mutex<Option<String>>>,
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(())
}
}

View file

@ -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<Mutex<Option<ProgressNotification>>>,
state: Option<ProgressNotification>,
///
pub fn get_progress_percent(&self) -> u8 {
self.progress.progress
}
pub(crate) fn set_progress<T>(
progress: Arc<Mutex<Option<T>>>,
state: Option<T>,
) -> 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<AsyncNotification>,
receiver: Receiver<ProgressNotification>,
progress: Arc<Mutex<Option<ProgressNotification>>>,
receiver: Receiver<T>,
progress: Arc<Mutex<Option<T>>>,
) -> 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<ProgressNotification> 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);
}
}

View file

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

View file

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

View file

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

View file

@ -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<BasicAuthCredential>,
) -> Result<Vec<String>> {
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::<Vec<_>>();
Ok(remote_tags)
}
/// lists the remotes tags missing
fn tags_missing_remote(
repo_path: &str,
remote: &str,
basic_credential: Option<BasicAuthCredential>,
) -> Result<Vec<String>> {
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::<HashSet<_>>();
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<BasicAuthCredential>,
progress_sender: Option<Sender<PushTagsProgress>>,
) -> 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());
}
}

View file

@ -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<AsyncNotification>,
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)?;

View file

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

View file

@ -158,7 +158,7 @@ impl PushComponent {
|progress| {
(
Self::progress_state_name(&progress.state),
progress.progress,
progress.get_progress_percent(),
)
},
)

263
src/components/push_tags.rs Normal file
View file

@ -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<PushTagsProgress>,
pending: bool,
queue: Queue,
theme: SharedTheme,
key_config: SharedKeyConfig,
input_cred: CredComponent,
}
impl PushTagsComponent {
///
pub fn new(
queue: &Queue,
sender: &Sender<AsyncNotification>,
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<BasicAuthCredential>,
) -> 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<PushTagsProgress>,
) -> (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<B: Backend>(
&self,
f: &mut Frame<B>,
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<CommandInfo>,
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<bool> {
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(())
}
}

View file

@ -65,6 +65,8 @@ pub enum InternalEvent {
Push(String, bool),
///
Pull(String),
///
PushTags,
}
///

View file

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

View file

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