use crate::bounded_push::positive_seconds;
use std::ffi::OsStr;
use std::io::Write;
use std::time::{Duration, Instant};
pub const NOTICE_INTERVAL_VARIABLE: &str = "CLI_GIT_WAIT_NOTICE_SECONDS";
pub const DEFAULT_NOTICE_INTERVAL: Duration = Duration::from_secs(10);
pub const NOTICE_GRACE: Duration = Duration::from_secs(1);
const SHORTEST_INTERVAL: Duration = Duration::from_millis(1);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Cadence {
pub grace: Duration,
pub interval: Duration,
}
impl Cadence {
pub fn from_interval(interval: Duration) -> Self {
return Self {
grace: interval.min(NOTICE_GRACE),
interval,
};
}
pub fn standard() -> Self {
return Self::from_interval(DEFAULT_NOTICE_INTERVAL);
}
pub fn from_value(value: Option<&OsStr>) -> Self {
let Some(text) = value else {
return Self::standard();
};
return match positive_seconds(NOTICE_INTERVAL_VARIABLE, text) {
Some(interval) => Self::from_interval(interval),
None => Self::standard(),
};
}
pub fn from_environment() -> Self {
return Self::from_value(std::env::var_os(NOTICE_INTERVAL_VARIABLE).as_deref());
}
}
#[derive(Clone, Copy, Debug)]
pub struct Ticker {
interval: Duration,
first_at: Duration,
printed: u32,
}
impl Ticker {
pub fn new(cadence: Cadence) -> Self {
return Self {
interval: cadence.interval.max(SHORTEST_INTERVAL),
first_at: cadence.grace,
printed: 0,
};
}
pub fn announced_at_start(cadence: Cadence) -> Self {
return Self {
interval: cadence.interval.max(SHORTEST_INTERVAL),
first_at: Duration::ZERO,
printed: 1,
};
}
fn due_count(&self, waited: Duration) -> u32 {
if waited < self.first_at {
return 0;
}
let after_first: u128 = (waited - self.first_at).as_nanos() / self.interval.as_nanos();
return u32::try_from(after_first.saturating_add(1)).unwrap_or(u32::MAX);
}
pub fn due(&mut self, waited: Duration) -> bool {
let count: u32 = self.due_count(waited);
if count <= self.printed {
return false;
}
self.printed = count;
return true;
}
pub fn next_in(&self, waited: Duration) -> Duration {
let due_at: Duration = self.first_at.saturating_add(self.interval.saturating_mul(self.printed));
return due_at.saturating_sub(waited);
}
}
#[derive(Clone, Copy, Debug)]
pub struct ChildWaitFacts<'facts> {
pub waiting_for: &'facts str,
pub pid: u32,
pub waited: Duration,
pub limit: Duration,
pub limit_note: &'facts str,
pub repeat: bool,
pub ends_when_quiet: bool,
}
pub fn child_wait_line(facts: &ChildWaitFacts<'_>) -> String {
let hint: &str = if facts.repeat {
""
} else {
" Hint: it is still running; if it looks stuck, check it with `ps` and report it."
};
if !facts.ends_when_quiet {
return format!(
"cli-git: still waiting for {} (process {}) after {} seconds; it is a step that may not be run twice, so it is \
waited for as long as it lives{}.{hint}",
facts.waiting_for,
facts.pid,
facts.waited.as_secs(),
facts.limit_note
);
}
return format!(
"cli-git: still waiting for {} (process {}) after {} seconds; it is ended only after {} seconds with no sign of work{}.{hint}",
facts.waiting_for,
facts.pid,
facts.waited.as_secs(),
facts.limit.as_secs(),
facts.limit_note
);
}
pub struct WaitReporter<'sink> {
started: Instant,
ticker: Ticker,
sink: &'sink mut dyn Write,
printed: u32,
}
impl<'sink> WaitReporter<'sink> {
pub fn start(cadence: Cadence, sink: &'sink mut dyn Write) -> Self {
return Self {
started: Instant::now(),
ticker: Ticker::new(cadence),
sink,
printed: 0,
};
}
pub fn poll(&mut self, line: impl FnOnce(Duration, bool) -> String) -> bool {
let waited: Duration = self.started.elapsed();
if !self.ticker.due(waited) {
return false;
}
let text: String = line(waited, self.printed > 0);
self.printed += 1;
let _written = writeln!(self.sink, "{text}");
return true;
}
}
#[cfg(test)]
#[path = "wait_progress_tests.rs"]
mod tests;