use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use std::time::Duration;
use gix::progress::prodash::progress::{Key, Task};
use gix::progress::tree::{Item, Root};
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
use crate::cli::stderr_color;
const TICK: Duration = Duration::from_millis(100);
const DOWNLOAD_TEMPLATE: &str =
"{spinner:.green} {msg} [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})";
const DOWNLOAD_TEMPLATE_NO_TOTAL: &str = "{spinner:.green} {msg} {bytes} ({bytes_per_sec})";
const STEP_TEMPLATE: &str = "{spinner:.cyan} {msg}";
const CLONE_TEMPLATE: &str =
"{spinner:.green} {msg} [{wide_bar:.cyan/blue}] {pos}/{len} ({percent}%, {eta})";
const CLONE_TEMPLATE_SPINNER: &str = "{spinner:.green} {msg}";
const PROGRESS_CHARS: &str = "##-";
pub struct FetchProgress {
multi: MultiProgress,
}
impl FetchProgress {
pub fn new() -> Self {
let multi = if stderr_color() {
MultiProgress::new()
} else {
MultiProgress::with_draw_target(ProgressDrawTarget::hidden())
};
Self { multi }
}
pub(crate) fn is_hidden(&self) -> bool {
self.multi.is_hidden()
}
pub(crate) fn download_bar(&self, label: &str, total: Option<u64>) -> GroupBar {
let pb = match total {
Some(len) => ProgressBar::new(len),
None => ProgressBar::new_spinner(),
};
let pb = self.multi.add(pb);
let template = if total.is_some() {
DOWNLOAD_TEMPLATE
} else {
DOWNLOAD_TEMPLATE_NO_TOTAL
};
pb.set_style(
ProgressStyle::with_template(template)
.expect("valid download template")
.progress_chars(PROGRESS_CHARS),
);
pb.set_message(label.to_string());
if !self.is_hidden() {
pb.enable_steady_tick(TICK);
}
GroupBar { pb }
}
pub(crate) fn step_bar(&self, label: &str) -> GroupBar {
let pb = self.multi.add(ProgressBar::new_spinner());
pb.set_style(ProgressStyle::with_template(STEP_TEMPLATE).expect("valid step template"));
pb.set_message(label.to_string());
if !self.is_hidden() {
pb.enable_steady_tick(TICK);
}
GroupBar { pb }
}
pub(crate) fn clone_progress(&self, label: &str) -> CloneProgress {
let root = Root::new();
let bar = self.multi.add(ProgressBar::new_spinner());
bar.set_style(
ProgressStyle::with_template(CLONE_TEMPLATE_SPINNER)
.expect("valid clone spinner template"),
);
bar.set_message(format!("cloning {label}"));
if self.is_hidden() {
return CloneProgress {
root,
bar,
stop: Arc::new(AtomicBool::new(false)),
poller: None,
};
}
bar.enable_steady_tick(TICK);
let stop = Arc::new(AtomicBool::new(false));
let poller = std::thread::spawn({
let root = Arc::clone(&root);
let bar = bar.clone();
let stop = Arc::clone(&stop);
let label = label.to_string();
move || poll_clone_tree(root, bar, stop, label)
});
CloneProgress {
root,
bar,
stop,
poller: Some(poller),
}
}
pub(crate) fn println(&self, line: &str) {
if self.is_hidden() {
eprintln!("{line}");
} else {
let _ = self.multi.println(line);
}
}
pub fn clear(&self) {
let _ = self.multi.clear();
}
}
impl Default for FetchProgress {
fn default() -> Self {
Self::new()
}
}
fn poll_clone_tree(root: Arc<Root>, bar: ProgressBar, stop: Arc<AtomicBool>, label: String) {
let mut snapshot: Vec<(Key, Task)> = Vec::new();
let mut determinate = false;
while !stop.load(Ordering::Relaxed) {
poll_tick(&root, &bar, &mut determinate, &label, &mut snapshot);
std::thread::sleep(TICK);
}
}
fn poll_tick(
root: &Root,
bar: &ProgressBar,
determinate: &mut bool,
label: &str,
snapshot: &mut Vec<(Key, Task)>,
) {
root.sorted_snapshot(snapshot);
let bounded = snapshot
.iter()
.rev()
.find(|(_, t)| t.progress.as_ref().and_then(|v| v.done_at).is_some());
let any = snapshot.iter().rev().find(|(_, t)| t.progress.is_some());
if let Some((_, task)) = bounded {
if let Some(value) = task.progress.as_ref() {
if !*determinate {
bar.set_style(
ProgressStyle::with_template(CLONE_TEMPLATE)
.expect("valid clone template")
.progress_chars(PROGRESS_CHARS),
);
*determinate = true;
}
bar.set_length(value.done_at.unwrap_or(0) as u64);
bar.set_position(value.step.load(Ordering::SeqCst) as u64);
bar.set_message(format!("{label}: {}", task.name));
}
} else {
if *determinate {
bar.set_style(
ProgressStyle::with_template(CLONE_TEMPLATE_SPINNER)
.expect("valid clone spinner template"),
);
*determinate = false;
}
if let Some((_, task)) = any {
bar.set_message(format!("{label}: {}", task.name));
}
}
}
pub(crate) struct GroupBar {
pb: ProgressBar,
}
impl GroupBar {
pub(crate) fn bar(&self) -> ProgressBar {
self.pb.clone()
}
pub(crate) fn finish(&self) {
self.pb.finish_and_clear();
}
}
impl Drop for GroupBar {
fn drop(&mut self) {
self.pb.finish_and_clear();
}
}
pub(crate) struct CloneProgress {
root: Arc<Root>,
bar: ProgressBar,
stop: Arc<AtomicBool>,
poller: Option<JoinHandle<()>>,
}
impl CloneProgress {
pub(crate) fn item(&self) -> Item {
self.root.add_child("clone")
}
pub(crate) fn finish(mut self) {
self.shutdown();
}
fn shutdown(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(handle) = self.poller.take() {
let _ = handle.join();
}
self.bar.finish_and_clear();
}
}
impl Drop for CloneProgress {
fn drop(&mut self) {
self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hidden_group() -> FetchProgress {
FetchProgress {
multi: MultiProgress::with_draw_target(ProgressDrawTarget::hidden()),
}
}
#[test]
fn fetch_progress_new_is_hidden_under_non_tty() {
assert!(
FetchProgress::new().is_hidden(),
"a non-TTY FetchProgress must use a hidden draw target so piped/CI stderr stays clean",
);
}
#[test]
fn download_bar_determinate_tracks_inc() {
let fp = hidden_group();
let bar = fp.download_bar("6.14.2", Some(100));
bar.pb.inc(40);
bar.pb.inc(60);
assert_eq!(bar.pb.position(), 100);
assert_eq!(bar.pb.length(), Some(100));
}
#[test]
fn download_bar_indeterminate_when_no_total() {
let fp = hidden_group();
let bar = fp.download_bar("6.15-rc3", None);
assert_eq!(bar.pb.length(), None);
bar.pb.inc(10);
assert_eq!(bar.pb.position(), 10);
}
#[test]
fn download_bar_finish_and_drop_no_panic() {
let fp = hidden_group();
let bar = fp.download_bar("k", Some(10));
bar.finish();
bar.finish();
drop(bar);
let bar2 = fp.download_bar("k2", Some(10));
bar2.pb.inc(5);
drop(bar2);
}
#[test]
fn fetch_progress_concurrent_bars_no_panic() {
let fp = hidden_group();
std::thread::scope(|s| {
for i in 0..16u32 {
let fp = &fp;
s.spawn(move || {
let bar = fp.download_bar(&format!("k{i}"), Some(50));
for _ in 0..50 {
bar.pb.inc(1);
}
assert_eq!(bar.pb.position(), 50, "each bar accounts independently");
bar.finish();
});
}
});
fp.clear();
}
#[test]
fn clone_progress_hidden_skips_poller() {
let fp = hidden_group();
let cp = fp.clone_progress("for-next");
assert!(
cp.poller.is_none(),
"hidden group must not spawn the poll thread",
);
let _item = cp.item();
cp.finish();
}
#[test]
fn clone_progress_finish_joins_poll_thread() {
let root = Root::new();
let bar = ProgressBar::hidden();
let stop = Arc::new(AtomicBool::new(false));
let poller = std::thread::spawn({
let root = Arc::clone(&root);
let bar = bar.clone();
let stop = Arc::clone(&stop);
move || poll_clone_tree(root, bar, stop, "for-next".to_string())
});
let cp = CloneProgress {
root,
bar,
stop,
poller: Some(poller),
};
cp.finish();
}
#[test]
fn poll_tick_maps_bounded_task_to_determinate_bar() {
let root = Root::new();
let bar = ProgressBar::hidden();
let child = root.add_child("receiving objects");
child.init(Some(200), None);
child.set(80);
let mut determinate = false;
let mut snapshot = Vec::new();
poll_tick(&root, &bar, &mut determinate, "for-next", &mut snapshot);
assert!(
determinate,
"a bounded task must switch the bar to determinate"
);
assert_eq!(bar.length(), Some(200));
assert_eq!(bar.position(), 80);
}
#[test]
fn poll_tick_unbounded_task_stays_spinner() {
let root = Root::new();
let bar = ProgressBar::hidden();
let child = root.add_child("negotiate");
child.init(None, None);
child.set(3);
let mut determinate = false;
let mut snapshot = Vec::new();
poll_tick(&root, &bar, &mut determinate, "for-next", &mut snapshot);
assert!(
!determinate,
"an unbounded task must not flip the bar to determinate",
);
assert_eq!(bar.length(), None, "no total ⇒ no determinate length");
}
#[test]
fn poll_tick_flips_back_to_spinner_when_unbounded() {
let root = Root::new();
let bar = ProgressBar::hidden();
let mut snapshot = Vec::new();
let mut determinate = false;
let bounded = root.add_child("resolving deltas");
bounded.init(Some(50), None);
bounded.set(10);
poll_tick(&root, &bar, &mut determinate, "for-next", &mut snapshot);
assert!(determinate, "a bounded task must flip to determinate first");
drop(bounded);
let unbounded = root.add_child("negotiate");
unbounded.init(None, None);
unbounded.set(1);
poll_tick(&root, &bar, &mut determinate, "for-next", &mut snapshot);
assert!(
!determinate,
"an unbounded-only snapshot must flip the bar back to spinner mode",
);
}
#[test]
fn fetch_progress_println_hidden_no_panic() {
let fp = hidden_group();
assert!(fp.is_hidden());
fp.println("cargo ktstr: downloading linux-6.14.2 (130.0 MiB)");
}
#[test]
fn concurrent_clone_progress_shutdown_no_panic() {
std::thread::scope(|s| {
for _ in 0..8 {
s.spawn(|| {
let root = Root::new();
let bar = ProgressBar::hidden();
let stop = Arc::new(AtomicBool::new(false));
let poller = std::thread::spawn({
let root = Arc::clone(&root);
let bar = bar.clone();
let stop = Arc::clone(&stop);
move || poll_clone_tree(root, bar, stop, "ref".to_string())
});
let cp = CloneProgress {
root,
bar,
stop,
poller: Some(poller),
};
let _ = cp.item();
cp.finish();
});
}
});
}
#[test]
fn step_bar_no_counter_no_panic() {
let fp = hidden_group();
let bar = fp.step_bar("Building kernel...");
assert_eq!(
bar.pb.length(),
None,
"a step bar has no quantifiable total"
);
bar.finish();
drop(fp.step_bar("Configuring kernel..."));
}
#[test]
fn concurrent_step_bars_no_panic() {
let fp = hidden_group();
std::thread::scope(|s| {
for i in 0..16u32 {
let fp = &fp;
s.spawn(move || {
let bar = fp.step_bar(&format!("Building kernel {i}..."));
bar.finish();
});
}
});
fp.clear();
}
}