mirror of
https://github.com/gitui-org/gitui
synced 2026-05-22 16:38:28 +00:00
add support for pushing tags (#569)
This commit is contained in:
parent
0d9a973e79
commit
f1fcd341c6
18 changed files with 903 additions and 54 deletions
|
|
@ -1,3 +1,2 @@
|
|||
msrv = "1.50.0"
|
||||
cognitive-complexity-threshold = 18
|
||||
too-many-lines-threshold = 105
|
||||
cognitive-complexity-threshold = 18
|
||||
|
|
@ -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)**
|
||||
|
|
|
|||
|
|
@ -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
47
asyncgit/src/progress.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
153
asyncgit/src/push_tags.rs
Normal 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(¶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<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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
303
asyncgit/src/sync/remotes/tags.rs
Normal file
303
asyncgit/src/sync/remotes/tags.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
21
src/app.rs
21
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<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)?;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
263
src/components/push_tags.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -65,6 +65,8 @@ pub enum InternalEvent {
|
|||
Push(String, bool),
|
||||
///
|
||||
Pull(String),
|
||||
///
|
||||
PushTags,
|
||||
}
|
||||
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue