Skip to main content

grit_lib/
progress.rs

1//! Progress reporting and cancellation across the library/CLI boundary.
2//!
3//! Library operations never touch the terminal. Long-running work reports
4//! progress through a [`ProgressSink`] supplied by the caller and checks a
5//! [`Cancel`] signal at loop boundaries. The CLI provides a concrete sink that
6//! draws to stderr (gated on `isatty`) and decides colour, redraw rate, and
7//! whether a pager is attached; the library makes none of those decisions.
8//!
9//! Sinks are invoked from the **calling thread only**, at **coarse** granularity
10//! (phase boundaries and periodic counts, not per-object spam). Totals passed
11//! to [`ProgressSink::start`] may be [`None`] when the operation cannot know
12//! the final count up front.
13//!
14//! Tests, `grit-cli`, and any call site that does not want output use the
15//! no-op [`NullProgress`] / [`NeverCancel`].
16//!
17//! Library guide: [progress](https://grit-scm.com/docs/library/parallelism)
18//!
19//! ```rust
20//! use grit_lib::progress::{NeverCancel, NullProgress};
21//! # fn main() {
22//! let _sink = NullProgress;
23//! let _cancel = NeverCancel;
24//! # }
25//! ```
26
27mod estimate;
28
29pub use estimate::{OverallEstimate, OverallEstimator, PhaseRates};
30
31/// A coarse phase of a long-running Git operation.
32///
33/// New variants may be added over time; match exhaustively only in the CLI or
34/// with a wildcard arm when forwarding unknown phases.
35#[non_exhaustive]
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum ProgressPhase {
38    /// Receiving a pack or loose objects from a remote or bundle stream.
39    ReceivingObjects,
40    /// Building or reading a pack index after objects are on disk.
41    IndexingObjects,
42    /// Applying delta chains inside a pack.
43    ResolvingDeltas,
44    /// Copying objects between local repositories (e.g. local fetch).
45    CopyingObjects,
46    /// Updating the work tree to match a tree (checkout, reset --hard, clone).
47    CheckingOutFiles,
48}
49
50impl ProgressPhase {
51    /// Returns the unit callers should use when reporting counts for `self`.
52    #[must_use]
53    pub fn default_unit(self) -> ProgressUnit {
54        match self {
55            Self::ReceivingObjects
56            | Self::IndexingObjects
57            | Self::ResolvingDeltas
58            | Self::CopyingObjects => ProgressUnit::Objects,
59            Self::CheckingOutFiles => ProgressUnit::Files,
60        }
61    }
62}
63
64/// Unit for [`ProgressSink::inc`] / [`ProgressSink::set`] during a phase.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub enum ProgressUnit {
67    /// Git objects (commits, trees, blobs, tags).
68    Objects,
69    /// Raw bytes (e.g. pack stream length when known).
70    Bytes,
71    /// Work-tree paths.
72    Files,
73}
74
75/// How far a pack transfer has got, counted in objects rather than bytes.
76///
77/// Reported through [`crate::fetch::Progress::received_objects`] while a pack
78/// streams in, when the sink asks for it with
79/// [`crate::fetch::Progress::wants_received_objects`].
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub struct ReceivedObjects {
82    /// Objects fully received so far.
83    pub received: u64,
84    /// Objects in the pack, from its header.
85    pub total: u64,
86    /// How many of the received objects are deltas.
87    pub deltas: u64,
88}
89
90/// A sink for progress updates emitted by a library operation.
91///
92/// Every method has a no-op default, so a sink implements only what it needs.
93/// The library calls these from the caller's thread at coarse intervals; it
94/// never decides whether output is a tty, what colour to use, or how often to
95/// redraw — that is the CLI's responsibility.
96pub trait ProgressSink {
97    /// Begin a new progress `phase`, optionally with a known total count of
98    /// [`ProgressPhase::default_unit`] units for that phase.
99    ///
100    /// `total` may be [`None`] when the operation cannot estimate the final
101    /// count (common for network receives).
102    ///
103    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
104        let _ = (phase, total);
105    }
106
107    /// Begin a phase with an explicit count unit (for example [`ProgressUnit::Bytes`]
108    /// when streaming a pack whose size is known in bytes).
109    ///
110    /// Defaults to [`Self::start`]; override when the sink renders counts differently
111    /// per [`ProgressUnit`].
112    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
113        let _ = unit;
114        self.start(phase, total);
115    }
116
117    /// Advance the current phase by `units` in the active phase unit.
118    fn inc(&mut self, units: u64) {
119        let _ = units;
120    }
121
122    /// Set the absolute progress of the current phase to `current` units.
123    fn set(&mut self, current: u64) {
124        let _ = current;
125    }
126
127    /// Finish the current phase.
128    fn finish(&mut self) {}
129
130    /// When `true`, the operation should stop at the next [`poll_cancel`] check.
131    ///
132    /// CLI progress sinks may set this when the user interrupts (Ctrl+C). Most
133    /// call sites rely on a separate [`Cancel`] argument instead; either signal
134    /// produces [`crate::error::Error::Cancelled`].
135    fn should_cancel(&self) -> bool {
136        false
137    }
138}
139
140/// Forwarding impl so a `&mut P` can be threaded into sub-operations that also
141/// take `impl ProgressSink` without re-borrowing gymnastics at every call site.
142impl<T: ProgressSink + ?Sized> ProgressSink for &mut T {
143    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
144        (**self).start(phase, total);
145    }
146    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
147        (**self).start_with_unit(phase, unit, total);
148    }
149    fn inc(&mut self, units: u64) {
150        (**self).inc(units);
151    }
152    fn set(&mut self, current: u64) {
153        (**self).set(current);
154    }
155    fn finish(&mut self) {
156        (**self).finish();
157    }
158    fn should_cancel(&self) -> bool {
159        (**self).should_cancel()
160    }
161}
162
163/// A [`ProgressSink`] that discards every update. The default for tests,
164/// `grit-cli`, and any call site that does not want progress output.
165#[derive(Debug, Default, Clone, Copy)]
166pub struct NullProgress;
167
168impl ProgressSink for NullProgress {}
169
170/// A cancellation signal checked by long-running library operations at loop
171/// boundaries. Implementations must be cheap to query.
172pub trait Cancel {
173    /// Returns `true` once the operation should stop as soon as possible.
174    fn is_cancelled(&self) -> bool;
175
176    /// When cancellation is backed by a shared flag (for example [`FlagCancel`]), returns it
177    /// so worker threads can observe the same signal.
178    fn shared_watch(&self) -> Option<std::sync::Arc<std::sync::atomic::AtomicBool>> {
179        None
180    }
181}
182
183/// A [`Cancel`] that never signals cancellation. The default for call sites
184/// that do not support interruption.
185#[derive(Debug, Default, Clone, Copy)]
186pub struct NeverCancel;
187
188impl Cancel for NeverCancel {
189    fn is_cancelled(&self) -> bool {
190        false
191    }
192}
193
194/// Shared flag for tests and embedders that flip cancellation mid-operation.
195#[derive(Debug, Clone)]
196pub struct FlagCancel(pub std::sync::Arc<std::sync::atomic::AtomicBool>);
197
198impl FlagCancel {
199    /// Create a cancel signal that starts unset.
200    #[must_use]
201    pub fn new() -> Self {
202        Self(std::sync::Arc::new(std::sync::atomic::AtomicBool::new(
203            false,
204        )))
205    }
206
207    /// Signal cancellation to every consumer polling this handle.
208    pub fn cancel(&self) {
209        self.0.store(true, std::sync::atomic::Ordering::Relaxed);
210    }
211}
212
213impl Default for FlagCancel {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219impl Cancel for FlagCancel {
220    fn is_cancelled(&self) -> bool {
221        self.0.load(std::sync::atomic::Ordering::Relaxed)
222    }
223
224    fn shared_watch(&self) -> Option<std::sync::Arc<std::sync::atomic::AtomicBool>> {
225        Some(std::sync::Arc::clone(&self.0))
226    }
227}
228
229/// Shared [`AtomicBool`](std::sync::atomic::AtomicBool) for cancellation visible across threads.
230///
231/// For [`FlagCancel`], this is the same flag as the handle (so embedders can cancel mid-scan).
232/// For other [`Cancel`] implementations, a private flag is seeded from [`Cancel::is_cancelled`]
233/// at creation time; combine with [`poll_cancel_watch`] on the main thread for progress signals.
234#[must_use]
235pub fn cancel_watch_arc(cancel: &dyn Cancel) -> std::sync::Arc<std::sync::atomic::AtomicBool> {
236    cancel.shared_watch().unwrap_or_else(|| {
237        std::sync::Arc::new(std::sync::atomic::AtomicBool::new(cancel.is_cancelled()))
238    })
239}
240
241/// Returns [`crate::error::Error::Cancelled`] when `watch` was set.
242#[inline]
243pub fn poll_watch(watch: &std::sync::atomic::AtomicBool) -> crate::error::Result<()> {
244    if watch.load(std::sync::atomic::Ordering::Relaxed) {
245        Err(crate::error::Error::Cancelled)
246    } else {
247        Ok(())
248    }
249}
250
251/// Like [`poll_cancel`], but also records cancellation in `watch` for worker threads.
252#[inline]
253pub fn poll_cancel_watch(
254    progress: &mut dyn ProgressSink,
255    cancel: &dyn Cancel,
256    watch: &std::sync::atomic::AtomicBool,
257) -> crate::error::Result<()> {
258    if cancel.is_cancelled() || progress.should_cancel() {
259        watch.store(true, std::sync::atomic::Ordering::Relaxed);
260        return Err(crate::error::Error::Cancelled);
261    }
262    poll_watch(watch)
263}
264
265/// [`ProgressSink`] adapter that mirrors [`ProgressSink::should_cancel`] into `watch`.
266pub struct WatchProgress<'a> {
267    inner: &'a mut dyn ProgressSink,
268    watch: std::sync::Arc<std::sync::atomic::AtomicBool>,
269}
270
271impl<'a> WatchProgress<'a> {
272    /// Wrap `inner` so any cancel request on the sink sets `watch`.
273    #[must_use]
274    pub fn new(
275        inner: &'a mut dyn ProgressSink,
276        watch: std::sync::Arc<std::sync::atomic::AtomicBool>,
277    ) -> Self {
278        Self { inner, watch }
279    }
280
281    fn note_cancel(&self) {
282        if self.inner.should_cancel() {
283            self.watch.store(true, std::sync::atomic::Ordering::Relaxed);
284        }
285    }
286}
287
288impl ProgressSink for WatchProgress<'_> {
289    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
290        self.note_cancel();
291        self.inner.start(phase, total);
292    }
293    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
294        self.note_cancel();
295        self.inner.start_with_unit(phase, unit, total);
296    }
297    fn inc(&mut self, units: u64) {
298        self.note_cancel();
299        self.inner.inc(units);
300    }
301    fn set(&mut self, current: u64) {
302        self.note_cancel();
303        self.inner.set(current);
304    }
305    fn finish(&mut self) {
306        self.note_cancel();
307        self.inner.finish();
308    }
309    fn should_cancel(&self) -> bool {
310        self.inner.should_cancel()
311    }
312}
313
314/// Returns [`crate::error::Error::Cancelled`] when `cancel` or `progress` request stop.
315#[inline]
316pub fn poll_cancel(
317    progress: &mut dyn ProgressSink,
318    cancel: &dyn Cancel,
319) -> crate::error::Result<()> {
320    if cancel.is_cancelled() || progress.should_cancel() {
321        Err(crate::error::Error::Cancelled)
322    } else {
323        Ok(())
324    }
325}
326
327/// [`ProgressSink`] that stops forwarding once [`Cancel`] or [`ProgressSink::should_cancel`] fires.
328///
329/// Long checkout loops call [`Self::finish_poll`] after apply so callers get [`crate::error::Error::Cancelled`]
330/// even when cancellation happened on an [`ProgressSink::inc`] boundary.
331pub struct CancelProgress<'a> {
332    inner: &'a mut dyn ProgressSink,
333    cancel: &'a dyn Cancel,
334    stopped: std::cell::Cell<bool>,
335}
336
337impl<'a> CancelProgress<'a> {
338    /// Wrap `inner` so every progress update observes `cancel`.
339    pub fn new(inner: &'a mut dyn ProgressSink, cancel: &'a dyn Cancel) -> Self {
340        Self {
341            inner,
342            cancel,
343            stopped: std::cell::Cell::new(false),
344        }
345    }
346
347    /// Whether a cancel signal was observed.
348    #[must_use]
349    pub fn stopped(&self) -> bool {
350        self.stopped.get()
351    }
352
353    /// Returns [`crate::error::Error::Cancelled`] when [`Self::stopped`].
354    pub fn finish_poll(&self) -> crate::error::Result<()> {
355        if self.stopped() {
356            Err(crate::error::Error::Cancelled)
357        } else {
358            Ok(())
359        }
360    }
361
362    fn note_stop(&self) {
363        if self.cancel.is_cancelled() || self.inner.should_cancel() {
364            self.stopped.set(true);
365        }
366    }
367}
368
369impl ProgressSink for CancelProgress<'_> {
370    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
371        self.note_stop();
372        if !self.stopped.get() {
373            self.inner.start(phase, total);
374        }
375    }
376
377    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
378        self.note_stop();
379        if !self.stopped.get() {
380            self.inner.start_with_unit(phase, unit, total);
381        }
382    }
383
384    fn inc(&mut self, units: u64) {
385        self.note_stop();
386        if !self.stopped.get() {
387            self.inner.inc(units);
388        }
389    }
390
391    fn set(&mut self, current: u64) {
392        self.note_stop();
393        if !self.stopped.get() {
394            self.inner.set(current);
395        }
396    }
397
398    fn finish(&mut self) {
399        self.note_stop();
400        if !self.stopped.get() {
401            self.inner.finish();
402        }
403    }
404
405    fn should_cancel(&self) -> bool {
406        self.stopped.get() || self.cancel.is_cancelled() || self.inner.should_cancel()
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413    use crate::fetch::{NoProgress, Progress};
414
415    /// A sink that records typed phase updates, to prove the trait + forwarding
416    /// impl route calls correctly.
417    #[derive(Default)]
418    struct PhaseRecorder {
419        started: Vec<(ProgressPhase, Option<u64>)>,
420        incs: u64,
421        finished: usize,
422    }
423
424    impl ProgressSink for PhaseRecorder {
425        fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
426            self.started.push((phase, total));
427        }
428        fn inc(&mut self, units: u64) {
429            self.incs += units;
430        }
431        fn finish(&mut self) {
432            self.finished += 1;
433        }
434    }
435
436    fn drive_typed(sink: &mut impl ProgressSink) {
437        sink.start(ProgressPhase::ReceivingObjects, Some(3));
438        sink.inc(1);
439        sink.inc(2);
440        sink.finish();
441    }
442
443    #[test]
444    fn null_progress_is_inert() {
445        drive_typed(&mut NullProgress);
446    }
447
448    #[test]
449    fn no_progress_is_inert() {
450        drive_typed(&mut NoProgress);
451    }
452
453    #[test]
454    fn typed_phase_recorder_roundtrip() {
455        let mut rec = PhaseRecorder::default();
456        drive_typed(&mut &mut rec);
457        assert_eq!(
458            rec.started,
459            vec![(ProgressPhase::ReceivingObjects, Some(3))]
460        );
461        assert_eq!(rec.incs, 3);
462        assert_eq!(rec.finished, 1);
463    }
464
465    #[test]
466    fn progress_supertrait_upcasts() {
467        struct SidebandCap(Vec<u8>);
468
469        impl ProgressSink for SidebandCap {}
470
471        impl Progress for SidebandCap {
472            fn message(&mut self, bytes: &[u8]) {
473                self.0.extend_from_slice(bytes);
474            }
475        }
476
477        fn takes_sink(sink: &mut dyn ProgressSink) {
478            sink.start(ProgressPhase::IndexingObjects, None);
479            sink.finish();
480        }
481
482        let mut cap = SidebandCap(Vec::new());
483        let progress: &mut dyn Progress = &mut cap;
484        takes_sink(progress);
485    }
486
487    #[test]
488    fn never_cancel_never_cancels() {
489        assert!(!NeverCancel.is_cancelled());
490    }
491
492    #[test]
493    fn poll_cancel_returns_typed_error() {
494        let flag = FlagCancel::new();
495        flag.cancel();
496        let mut sink = NullProgress;
497        let err = poll_cancel(&mut sink, &flag).expect_err("cancelled");
498        assert!(matches!(err, crate::error::Error::Cancelled));
499    }
500
501    #[test]
502    fn phase_default_unit() {
503        assert_eq!(
504            ProgressPhase::ReceivingObjects.default_unit(),
505            ProgressUnit::Objects
506        );
507        assert_eq!(
508            ProgressPhase::CheckingOutFiles.default_unit(),
509            ProgressUnit::Files
510        );
511    }
512}