diff --git a/libra/src/command/fetch.rs b/libra/src/command/fetch.rs index 417e66cb9..9ab96ff92 100644 --- a/libra/src/command/fetch.rs +++ b/libra/src/command/fetch.rs @@ -6,12 +6,12 @@ use ceres::protocol::ServiceType::UploadPack; use clap::Parser; use indicatif::ProgressBar; use mercury::internal::object::commit::Commit; -use mercury::{errors::GitError, hash::SHA1}; +use mercury::hash::SHA1; use tokio::io::{AsyncRead, AsyncReadExt}; use tokio_util::io::StreamReader; use url::Url; -use crate::command::{ask_basic_auth, load_object}; +use crate::command::load_object; use crate::{ command::index_pack::{self, IndexPackArgs}, internal::{ @@ -89,20 +89,14 @@ pub async fn fetch_repository(remote_config: &RemoteConfig, branch: Option refs, + Err(e) => { eprintln!("fatal: {}", e); return; } - } - let refs = refs.unwrap(); + }; + if refs.is_empty() { tracing::warn!("fetch empty, no refs found"); return; @@ -133,7 +127,7 @@ pub async fn fetch_repository(remote_config: &RemoteConfig, branch: Option { + LfsCmds::Untrack { path } => { // only remove totally same pattern with path ? let path = convert_patterns_to_workdir(path); // untrack_lfs_patterns(&attr_path, path).unwrap(); } @@ -110,19 +110,13 @@ pub async fn execute(cmd: LfsCmds) { } let refspec = current_refspec().await.unwrap(); - let mut auth = None; - loop { - let code = LFSClient::get().await.lock(path.clone(), refspec.clone(), auth.clone()).await; - if code.is_success() { - println!("Locked {}", path); - } else if code == StatusCode::FORBIDDEN { - eprintln!("Forbidden: You must have push access to create a lock"); - auth = Some(ask_basic_auth()); - continue; - } else if code == StatusCode::CONFLICT { - eprintln!("Conflict: already created lock"); - } - break; + let code = LFSClient::get().await.lock(path.clone(), refspec.clone()).await; + if code.is_success() { + println!("Locked {}", path); + } else if code == StatusCode::FORBIDDEN { + eprintln!("Forbidden: You must have push access to create a lock"); + } else if code == StatusCode::CONFLICT { + eprintln!("Conflict: already created lock"); } } LfsCmds::Unlock { path, force, id } => { @@ -155,17 +149,11 @@ pub async fn execute(cmd: LfsCmds) { } Some(id) => id }; - let mut auth = None; - loop { - let code = LFSClient::get().await.unlock(id.clone(), refspec.clone(), force, auth.clone()).await; - if code.is_success() { - println!("Unlocked {}", path); - } else if code == StatusCode::FORBIDDEN { - eprintln!("Forbidden: You must have push access to unlock"); - auth = Some(ask_basic_auth()); - continue; - } - break; + let code = LFSClient::get().await.unlock(id.clone(), refspec.clone(), force).await; + if code.is_success() { + println!("Unlocked {}", path); + } else if code == StatusCode::FORBIDDEN { + eprintln!("Forbidden: You must have push access to unlock"); } } LfsCmds::LsFiles { long, size, name_only} => { diff --git a/libra/src/command/push.rs b/libra/src/command/push.rs index 8c281ec9b..68e814cd7 100644 --- a/libra/src/command/push.rs +++ b/libra/src/command/push.rs @@ -8,18 +8,17 @@ use tokio::sync::mpsc; use url::Url; use ceres::protocol::ServiceType::ReceivePack; use ceres::protocol::smart::{add_pkt_line_string, read_pkt_line}; -use mercury::errors::GitError; use mercury::hash::SHA1; use mercury::internal::object::blob::Blob; use mercury::internal::object::commit::Commit; use mercury::internal::object::tree::{Tree, TreeItemMode}; use mercury::internal::pack::encode::PackEncoder; use mercury::internal::pack::entry::Entry; -use crate::command::{ask_basic_auth, branch}; +use crate::command::branch; use crate::internal::branch::Branch; use crate::internal::config::Config; use crate::internal::head::Head; -use crate::internal::protocol::https_client::{BasicAuth, HttpsClient}; +use crate::internal::protocol::https_client::HttpsClient; use crate::internal::protocol::lfs_client::LFSClient; use crate::internal::protocol::ProtocolClient; use crate::utils::object_ext::{BlobExt, CommitExt, TreeExt}; @@ -74,18 +73,13 @@ pub async fn execute(args: PushArgs) { let url = Url::parse(&repo_url).unwrap(); let client = HttpsClient::from_url(&url); - let mut refs = client.discovery_reference(ReceivePack, None).await; - let mut auth: Option = None; - while let Err(e) = refs { // retry if unauthorized - if let GitError::UnAuthorized(_) = e { - auth = Some(ask_basic_auth()); - refs = client.discovery_reference(ReceivePack, auth.clone()).await; - } else { + let refs = match client.discovery_reference(ReceivePack).await { + Ok(refs) => refs, + Err(e) => { eprintln!("fatal: {}", e); return; } - } - let refs = refs.unwrap(); + }; let tracked_branch = Config::get("branch", Some(&branch), "merge") .await // New branch may not have tracking branch @@ -115,7 +109,7 @@ pub async fn execute(args: PushArgs) { { // upload lfs files let client = LFSClient::from_url(&url); - let res = client.push_objects(&objs, auth.clone()).await; + let res = client.push_objects(&objs).await; if res.is_err() { eprintln!("fatal: LFS files upload failed, stop pushing"); return; @@ -143,7 +137,7 @@ pub async fn execute(args: PushArgs) { data.extend_from_slice(&pack_data); println!("Delta compression done."); - let res = client.send_pack(data.freeze(), auth).await.unwrap(); // TODO: send stream + let res = client.send_pack(data.freeze()).await.unwrap(); // TODO: send stream if res.status() != 200 { eprintln!("status code: {}", res.status()); diff --git a/libra/src/internal/protocol/https_client.rs b/libra/src/internal/protocol/https_client.rs index 8b7878dbc..2955d205b 100644 --- a/libra/src/internal/protocol/https_client.rs +++ b/libra/src/internal/protocol/https_client.rs @@ -7,10 +7,13 @@ use futures_util::{StreamExt, TryStreamExt}; use mercury::errors::GitError; use mercury::hash::SHA1; use reqwest::header::CONTENT_TYPE; -use reqwest::{Body, Response}; +use reqwest::{Body, RequestBuilder, Response, StatusCode}; use std::io::Error as IoError; +use std::ops::Deref; +use std::sync::Mutex; use tokio_util::bytes::BytesMut; use url::Url; +use crate::command::ask_basic_auth; /// A Git protocol client that communicates with a Git server over HTTPS. /// Only support `SmartProtocol` now, see [http-protocol](https://www.git-scm.com/docs/http-protocol) for protocol details. @@ -41,6 +44,41 @@ pub struct BasicAuth { pub(crate) password: String, } +impl BasicAuth { + /// send request with basic auth, retry 3 times + pub async fn send(request_builder: impl Fn() -> Fut) -> Result + where + Fut: std::future::Future, + { + static AUTH: Mutex> = Mutex::new(None); + const MAX_TRY: usize = 3; + let mut res; + let mut try_cnt = 0; + loop { + let mut request = request_builder().await; // RequestBuilder can't be cloned + if let Some(auth) = AUTH.lock().unwrap().deref() { + request = request.basic_auth(auth.username.clone(), Some(auth.password.clone())); + } // if no auth exists, try without auth (e.g. clone public) + res = request.send().await?; + if res.status() == StatusCode::FORBIDDEN { // 403: no access, no need to retry + eprintln!("Authentication failed, forbidden"); + break; + } else if res.status() != StatusCode::UNAUTHORIZED { + break; + } + // 401 (Unauthorized): username or password is incorrect + if try_cnt >= MAX_TRY { + eprintln!("Failed to authenticate after {} attempts", MAX_TRY); + break; + } + eprintln!("Authentication required, retrying..."); + AUTH.lock().unwrap().replace(ask_basic_auth()); + try_cnt += 1; + } + Ok(res) + } +} + #[derive(Debug, Clone, PartialEq)] pub struct DiscoveredReference { pub(crate) _hash: String, @@ -61,18 +99,13 @@ impl HttpsClient { pub async fn discovery_reference( &self, service: ServiceType, - auth: Option, ) -> Result, GitError> { let service: &str = &service.to_string(); let url = self .url .join(&format!("info/refs?service={}", service)) .unwrap(); - let mut request = self.client.get(url); - if let Some(auth) = auth { - request = request.basic_auth(auth.username, Some(auth.password)); - } - let res = request.send().await.unwrap(); + let res = BasicAuth::send(|| async{self.client.get(url.clone())}).await.unwrap(); tracing::debug!("{:?}", res); if res.status() == 401 { @@ -165,22 +198,19 @@ impl HttpsClient { &self, have: &Vec, want: &Vec, - auth: Option, ) -> Result>, IoError> { // POST $GIT_URL/git-upload-pack HTTP/1.0 let url = self.url.join("git-upload-pack").unwrap(); let body = generate_upload_pack_content(have, want).await; tracing::debug!("fetch_objects with body: {:?}", body); - let mut req = self - .client - .post(url) - .header("Content-Type", "application/x-git-upload-pack-request") - .body(body); - if let Some(auth) = auth { - req = req.basic_auth(auth.username, Some(auth.password)); - } - let res = req.send().await.unwrap(); + let res = BasicAuth::send(|| async { + self + .client + .post(url.clone()) + .header("Content-Type", "application/x-git-upload-pack-request") + .body(body.clone()) + }).await.unwrap(); tracing::debug!("request: {:?}", res); if res.status() != 200 && res.status() != 304 { @@ -197,22 +227,17 @@ impl HttpsClient { Ok(result) } - pub async fn send_pack>( + pub async fn send_pack + Clone>( &self, data: T, - auth: Option, ) -> Result { - let mut request = self - .client - .post(self.url.join("git-receive-pack").unwrap()) - .header(CONTENT_TYPE, "application/x-git-receive-pack-request") - .body(data); - - if let Some(auth) = auth { - request = request.basic_auth(auth.username, Some(auth.password)); - } - - request.send().await + BasicAuth::send(|| async { + self + .client + .post(self.url.join("git-receive-pack").unwrap()) + .header(CONTENT_TYPE, "application/x-git-receive-pack-request") + .body(data.clone()) + }).await } } /// for fetching @@ -259,7 +284,7 @@ mod tests { let test_repo = "https://github.com/web3infra-foundation/mega.git/"; let client = HttpsClient::from_url(&Url::parse(test_repo).unwrap()); - let refs = client.discovery_reference(UploadPack, None).await; + let refs = client.discovery_reference(UploadPack).await; if refs.is_err() { tracing::error!("{:?}", refs.err().unwrap()); panic!(); @@ -276,7 +301,7 @@ mod tests { let test_repo = "https://github.com/web3infra-foundation/mega/"; let client = HttpsClient::from_url(&Url::parse(test_repo).unwrap()); - let refs = client.discovery_reference(UploadPack, None).await.unwrap(); + let refs = client.discovery_reference(UploadPack).await.unwrap(); let refs: Vec = refs .iter() .filter(|r| r._ref.starts_with("refs/heads")) @@ -287,7 +312,7 @@ mod tests { let want = refs.iter().map(|r| r._hash.clone()).collect(); let have = vec!["81a162e7b725bbad2adfe01879fd57e0119406b9".to_string()]; - let mut result_stream = client.fetch_objects(&have, &want, None).await.unwrap(); + let mut result_stream = client.fetch_objects(&have, &want).await.unwrap(); let mut buffer = vec![]; while let Some(item) = result_stream.next().await { diff --git a/libra/src/internal/protocol/lfs_client.rs b/libra/src/internal/protocol/lfs_client.rs index 5b2af350a..9f14f1656 100644 --- a/libra/src/internal/protocol/lfs_client.rs +++ b/libra/src/internal/protocol/lfs_client.rs @@ -2,7 +2,7 @@ use crate::command; use crate::internal::config::Config; use crate::internal::protocol::https_client::BasicAuth; use crate::internal::protocol::ProtocolClient; -use crate::utils::lfs; +use crate::utils::{lfs, util}; use ceres::lfs::lfs_structs::{BatchRequest, ChunkRepresentation, FetchchunkResponse, LockList, LockListQuery, LockRequest, ObjectError, Ref, Representation, RequestVars, UnlockRequest, VerifiableLockList, VerifiableLockRequest}; use futures_util::StreamExt; use mercury::internal::object::types::ObjectType; @@ -90,7 +90,7 @@ impl LFSClient { } /// push LFS objects to remote server - pub async fn push_objects<'a, I>(&self, objs: I, auth: Option) -> Result<(), ()> + pub async fn push_objects<'a, I>(&self, objs: I) -> Result<(), ()> where I: IntoIterator { @@ -127,7 +127,7 @@ impl LFSClient { let (code, locks) = self.verify_locks(VerifiableLockRequest { refs: Ref { name: command::lfs::current_refspec().await.unwrap() }, ..Default::default() - }, auth.clone()).await; + }).await; if code == StatusCode::FORBIDDEN { eprintln!("fatal: Forbidden: You must have push access to verify locks"); @@ -173,15 +173,12 @@ impl LFSClient { hash_algo: lfs::LFS_HASH_ALGO.to_string(), }; - let mut request = self.client - .post(self.batch_url.clone()) - .json(&batch_request) - .headers(lfs::LFS_HEADERS.clone()); - if let Some(auth) = auth { - request = request.basic_auth(auth.username, Some(auth.password)); - } - - let response = request.send().await.unwrap(); + let response = BasicAuth::send(|| async { + self.client + .post(self.batch_url.clone()) + .json(&batch_request) + .headers(lfs::LFS_HEADERS.clone()) + }).await.unwrap(); let resp = response.json::().await.unwrap(); tracing::debug!("LFS push response:\n {:#?}", serde_json::to_value(&resp).unwrap()); @@ -208,23 +205,19 @@ impl LFSClient { hash_algo: lfs::LFS_HASH_ALGO.to_string(), }; - let request = self - .client - .post(self.batch_url.clone()) - .json(&batch_request) - .headers(lfs::LFS_HEADERS.clone()); - - let response = request.send().await.unwrap(); + let response = BasicAuth::send(|| async { + self.client + .post(self.batch_url.clone()) + .json(&batch_request) + .headers(lfs::LFS_HEADERS.clone()) + }).await.unwrap(); let resp = response.json::().await.unwrap(); tracing::debug!( "LFS push response:\n {:#?}", serde_json::to_value(&resp).unwrap() ); - assert!( - resp.objects.len() == 1, - "fatal: LFS push failed. No object found." - ); + assert_eq!(resp.objects.len(), 1, "fatal: LFS push failed. No object found."); // self.upload_object(resp.objects).await?; let obj = resp.objects.into_iter().next().unwrap(); @@ -247,21 +240,28 @@ impl LFSClient { return Err(()); } + println!("Uploading LFS file: {}", object.oid); let link = upload_link.unwrap(); - let mut request = self.client.put(&link.href); - for (k, v) in &link.header { - request = request.header(k, v); - } - let content = tokio::fs::File::open(file).await.unwrap(); - println!("Uploading LFS file: {}", object.oid); - let resp = request - .body(reqwest::Body::wrap_stream( - tokio_util::io::ReaderStream::new(content), - )) - .send() - .await - .unwrap(); + let resp = BasicAuth::send(|| async { + let mut request = self.client.put(&link.href); + for (k, v) in &link.header { + request = request.header(k, v); + } + + let content = tokio::fs::File::open(file).await.unwrap(); + let progress_bar = util::default_progress_bar(content.metadata().await.unwrap().len()); + + let stream = tokio_util::io::ReaderStream::new(content); + let progress_stream = stream.map(move |chunk| { + if let Ok(ref data) = chunk { + progress_bar.inc(data.len() as u64); + } + chunk + }); + request.body(reqwest::Body::wrap_stream(progress_stream)) + }).await.unwrap(); + if !resp.status().is_success() { eprintln!("fatal: LFS upload failed. Status: {}, Message: {}", resp.status(), resp.text().await.unwrap()); return Err(()); @@ -309,11 +309,12 @@ impl LFSClient { hash_algo: lfs::LFS_HASH_ALGO.to_string(), }; - let request = self.client - .post(self.batch_url.clone()) - .json(&batch_request) - .headers(lfs::LFS_HEADERS.clone()); - let response = request.send().await?; + let response = BasicAuth::send(|| async { + self.client + .post(self.batch_url.clone()) + .json(&batch_request) + .headers(lfs::LFS_HEADERS.clone()) + }).await?; let text = response.text().await?; tracing::debug!("LFS download response:\n {:#?}", serde_json::from_str::(&text)?); @@ -385,19 +386,25 @@ impl LFSClient { println!("- part: {}/{}", got_parts, parts); } - let mut request = self.client.get(&link.href); - for (k, v) in &link.header { - request = request.header(k, v); - } - - let response = request.send().await?; + let response = BasicAuth::send(|| async { + let mut request = self.client.get(&link.href); + for (k, v) in &link.header { + request = request.header(k, v); + } + request + }).await?; if !response.status().is_success() { eprintln!("fatal: LFS download failed. Status: {}, Message: {}", response.status(), response.text().await?); return Err(anyhow!("LFS download failed.")); } + let cur_chunk_size = if (got_parts as usize) < parts { + chunk_size.unwrap() as u64 + } else { // last part + size - (parts as u64 - 1) * chunk_size.unwrap() as u64 + }; + let pb = util::default_progress_bar(cur_chunk_size); let mut stream = response.bytes_stream(); - while let Some(chunk) = stream.next().await { // TODO: progress bar TODO: multi-thread or async let chunk = chunk?; file.write_all(&chunk).await?; @@ -411,8 +418,11 @@ impl LFSClient { last_progress = progress; report_fn(progress)?; } + } else { // mutually exclusive with reporter + pb.inc(chunk.len() as u64); } } + pb.finish_and_clear(); } let checksum = hex::encode(checksum.finish().as_ref()); if checksum == oid { @@ -434,8 +444,7 @@ impl LFSClient { let path = url.path().trim_end_matches('/'); url.set_path(&(path.to_owned() + "/chunks")); // reserve query params (for GitHub link) - let request = self.client.get(url); - let resp = request.send().await.unwrap(); + let resp = BasicAuth::send(|| async { self.client.get(url.clone()) }).await.unwrap(); let code = resp.status(); if code == StatusCode::NOT_FOUND || code == StatusCode::FORBIDDEN { // GitHub maybe return 403 tracing::info!("Remote LFS Server not support Chunks API, or forbidden."); @@ -451,18 +460,16 @@ impl LFSClient { } } -#[cfg(feature="p2p")] -type Reporter = (dyn FnMut(f64) -> anyhow::Result<()> + Send); #[cfg(feature="p2p")] impl LFSClient{ - /// download (GET) one LFS file peer-to-peer + #[allow(clippy::type_complexity)] pub async fn download_object_p2p( &self, file_uri: &str, // p2p protocol path: impl AsRef, mut reporter: Option<( - &mut Reporter, // progress callback + &mut (dyn FnMut(f64) -> anyhow::Result<()> + Send), // progress callback f64 // step )>) -> anyhow::Result<()> { @@ -484,7 +491,7 @@ impl LFSClient{ } tracing::debug!("P2P download tunnel ports: {:?}", peer_ports); - let lfs_info = match gemini::lfs::get_lfs_chunks_info(bootstrap_node.clone(), hash.clone()).await { + let lfs_info = match gemini::lfs::get_lfs_chunks_info(bootstrap_node.clone(), hash.clone()).await { // auth? Some(chunks) => chunks, None => return Err(anyhow!("fatal: LFS Chunk API failed.")) }; @@ -509,6 +516,7 @@ impl LFSClient{ let data = loop { // retry let mut downloaded = i * chunk_size; // TODO support resume let mut last_progress = downloaded as f64 / lfs_info.size as f64 * 100.0; + let pb = util::default_progress_bar(chunk.size as u64); let url = format!("http://localhost:{}/objects/{}/{}", peer_ports[(i + retry) % peer_ports.len()], hash, chunk.sub_oid); let data = self.download_chunk(&url, &chunk.sub_oid, chunk.size as usize, chunk.offset as usize, |size| { if let Some((ref mut report_fn, step)) = reporter { @@ -518,8 +526,11 @@ impl LFSClient{ last_progress = progress; report_fn(progress).unwrap(); } + } else { + pb.inc(size as u64); } }).await; + pb.finish_and_clear(); match data { Ok(data) => break data, Err(e) => { @@ -550,10 +561,11 @@ impl LFSClient{ } async fn download_chunk(&self, url: &str, hash: &str, size: usize, offset: usize, mut callback: impl FnMut(usize)) -> anyhow::Result> { - let response = self.client - .get(url) - .query(&[("offset", offset), ("size", size)]) - .send().await?; + let response = BasicAuth::send(|| async { + self.client + .get(url) + .query(&[("offset", offset), ("size", size)]) + }).await?; if !response.status().is_success() { eprintln!("fatal: LFS download failed. Status: {}, Message: {}", response.status(), response.text().await?); return Err(anyhow!("LFS download failed.")); @@ -582,16 +594,14 @@ impl LFSClient{ impl LFSClient { pub async fn get_locks(&self, query: LockListQuery) -> LockList { let url = self.lfs_url.join("locks").unwrap(); - let mut request = self.client.get(url); - request = request.query(&[ + let query = [ ("id", query.id), ("path", query.path), ("limit", query.limit), ("cursor", query.cursor), ("refspec", query.refspec) - ]); - let response = request.send().await.unwrap(); - + ]; + let response = BasicAuth::send(|| async { self.client.get(url.clone()).query(&query) }).await.unwrap(); if !response.status().is_success() { eprintln!("fatal: LFS get locks failed. Status: {}, Message: {}", response.status(), response.text().await.unwrap()); return LockList { @@ -605,16 +615,14 @@ impl LFSClient { /// lock an LFS file /// - `refspec` is must in Mega Server, but optional in Git Doc - pub async fn lock(&self, path: String, refspec: String, basic_auth: Option) -> StatusCode { + pub async fn lock(&self, path: String, refspec: String) -> StatusCode { let url = self.lfs_url.join("locks").unwrap(); - let mut request = self.client.post(url).json(&LockRequest { - path, - refs: Ref { name: refspec }, - }); - if let Some(auth) = basic_auth { - request = request.basic_auth(auth.username, Some(auth.password)); - } - let resp = request.send().await.unwrap(); + let resp = BasicAuth::send(|| async { + self.client.post(url.clone()).json(&LockRequest { + path: path.clone(), + refs: Ref { name: refspec.clone() }, + }) + }).await.unwrap(); let code = resp.status(); if !resp.status().is_success() && code != StatusCode::FORBIDDEN { eprintln!("fatal: LFS lock failed. Status: {}, Message: {}", code, resp.text().await.unwrap()); @@ -622,16 +630,14 @@ impl LFSClient { code } - pub async fn unlock(&self, id: String, refspec: String, force: bool, basic_auth: Option) -> StatusCode { + pub async fn unlock(&self, id: String, refspec: String, force: bool) -> StatusCode { let url = self.lfs_url.join(&format!("locks/{}/unlock", id)).unwrap(); - let mut request = self.client.post(url).json(&UnlockRequest { - force: Some(force), - refs: Ref { name: refspec }, - }); - if let Some(auth) = basic_auth.clone() { - request = request.basic_auth(auth.username, Some(auth.password)); - } - let resp = request.send().await.unwrap(); + let resp = BasicAuth::send(|| async { + self.client.post(url.clone()).json(&UnlockRequest { + force: Some(force), + refs: Ref { name: refspec.clone() }, + }) + }).await.unwrap(); let code = resp.status(); if !resp.status().is_success() && code != StatusCode::FORBIDDEN { eprintln!("fatal: LFS unlock failed. Status: {}, Message: {}", code, resp.text().await.unwrap()); @@ -640,15 +646,11 @@ impl LFSClient { } /// List Locks for Verification - pub async fn verify_locks(&self, query: VerifiableLockRequest, basic_auth: Option) + pub async fn verify_locks(&self, query: VerifiableLockRequest) -> (StatusCode, VerifiableLockList) { let url = self.lfs_url.join("locks/verify").unwrap(); - let mut request = self.client.post(url).json(&query); - if let Some(auth) = basic_auth { - request = request.basic_auth(auth.username, Some(auth.password)); - } - let resp = request.send().await.unwrap(); + let resp = BasicAuth::send(|| async {self.client.post(url.clone()).json(&query)}).await.unwrap(); let code = resp.status(); // By default, an LFS server that doesn't implement any locking endpoints should return 404. // This response will not halt any Git pushes. diff --git a/libra/src/utils/util.rs b/libra/src/utils/util.rs index daae3872e..979cad064 100644 --- a/libra/src/utils/util.rs +++ b/libra/src/utils/util.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use std::io::Write; use std::path::{Path, PathBuf}; use std::{env, fs, io}; - +use indicatif::{ProgressBar, ProgressStyle}; use mercury::hash::SHA1; use mercury::internal::object::types::ObjectType; @@ -327,6 +327,17 @@ pub fn auto_unit_bytes(bytes: u64) -> byte_unit::AdjustedByte { let bytes = byte_unit::Byte::from(bytes); bytes.get_appropriate_unit(byte_unit::UnitType::Binary) } +/// Create a default style progress bar +pub fn default_progress_bar(len: u64) -> ProgressBar { + let progress_bar = ProgressBar::new(len); + progress_bar.set_style( + ProgressStyle::default_bar() + .template("{spinner:.magenta} [{elapsed_precise}] [{bar:40.green/white}] {bytes}/{total_bytes} ({eta}) {bytes_per_sec}") + .unwrap() + .progress_chars("=> "), + ); + progress_bar +} #[cfg(test)] mod test { diff --git a/mega/tests/lfs_test.rs b/mega/tests/lfs_test.rs index b3d128b58..6cac27f86 100644 --- a/mega/tests/lfs_test.rs +++ b/mega/tests/lfs_test.rs @@ -53,9 +53,9 @@ fn run_cmd(program: &str, args: &[&str]) { .unwrap(); let status = output.status; + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); if !status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); panic!( "Command failed: {} {}\nStatus: {}\nStdout: {}\nStderr: {}", program, @@ -64,6 +64,14 @@ fn run_cmd(program: &str, args: &[&str]) { stdout, stderr ); + } else { + println!( + "Command success: {} {}\nStatus: {}\nStdout: {}", + program, + args.join(" "), + status, + stdout, + ); } }