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    /// Hashing work-tree files into the index (add, auto-staging commit, stash push).
49    StagingFiles,
50    /// Downloading or uploading a bundle that can resume where it stopped
51    /// (clone's `bundle-uri` bundle, a large push). Counted in bytes, including
52    /// any bytes an earlier run already moved; the detailed events go to
53    /// [`crate::fetch::Progress::resumable_transfer`].
54    TransferringBundle,
55}
56
57impl ProgressPhase {
58    /// Returns the unit callers should use when reporting counts for `self`.
59    #[must_use]
60    pub fn default_unit(self) -> ProgressUnit {
61        match self {
62            Self::ReceivingObjects
63            | Self::IndexingObjects
64            | Self::ResolvingDeltas
65            | Self::CopyingObjects => ProgressUnit::Objects,
66            Self::CheckingOutFiles | Self::StagingFiles => ProgressUnit::Files,
67            Self::TransferringBundle => ProgressUnit::Bytes,
68        }
69    }
70}
71
72/// Unit for [`ProgressSink::inc`] / [`ProgressSink::set`] during a phase.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum ProgressUnit {
75    /// Git objects (commits, trees, blobs, tags).
76    Objects,
77    /// Raw bytes (e.g. pack stream length when known).
78    Bytes,
79    /// Work-tree paths.
80    Files,
81}
82
83/// How far a pack transfer has got, counted in objects rather than bytes.
84///
85/// Reported through [`crate::fetch::Progress::received_objects`] while a pack
86/// streams in, when the sink asks for it with
87/// [`crate::fetch::Progress::wants_received_objects`].
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub struct ReceivedObjects {
90    /// Objects fully received so far.
91    pub received: u64,
92    /// Objects in the pack, from its header.
93    pub total: u64,
94    /// How many of the received objects are deltas.
95    pub deltas: u64,
96}
97
98/// A sink for progress updates emitted by a library operation.
99///
100/// Every method has a no-op default, so a sink implements only what it needs.
101/// The library calls these from the caller's thread at coarse intervals; it
102/// never decides whether output is a tty, what colour to use, or how often to
103/// redraw — that is the CLI's responsibility.
104pub trait ProgressSink {
105    /// Begin a new progress `phase`, optionally with a known total count of
106    /// [`ProgressPhase::default_unit`] units for that phase.
107    ///
108    /// `total` may be [`None`] when the operation cannot estimate the final
109    /// count (common for network receives).
110    ///
111    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
112        let _ = (phase, total);
113    }
114
115    /// Begin a phase with an explicit count unit (for example [`ProgressUnit::Bytes`]
116    /// when streaming a pack whose size is known in bytes).
117    ///
118    /// Defaults to [`Self::start`]; override when the sink renders counts differently
119    /// per [`ProgressUnit`].
120    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
121        let _ = unit;
122        self.start(phase, total);
123    }
124
125    /// Advance the current phase by `units` in the active phase unit.
126    fn inc(&mut self, units: u64) {
127        let _ = units;
128    }
129
130    /// Set the absolute progress of the current phase to `current` units.
131    fn set(&mut self, current: u64) {
132        let _ = current;
133    }
134
135    /// Finish the current phase.
136    fn finish(&mut self) {}
137
138    /// When `true`, the operation should stop at the next [`poll_cancel`] check.
139    ///
140    /// CLI progress sinks may set this when the user interrupts (Ctrl+C). Most
141    /// call sites rely on a separate [`Cancel`] argument instead; either signal
142    /// produces [`crate::error::Error::Cancelled`].
143    fn should_cancel(&self) -> bool {
144        false
145    }
146}
147
148/// Forwarding impl so a `&mut P` can be threaded into sub-operations that also
149/// take `impl ProgressSink` without re-borrowing gymnastics at every call site.
150impl<T: ProgressSink + ?Sized> ProgressSink for &mut T {
151    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
152        (**self).start(phase, total);
153    }
154    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
155        (**self).start_with_unit(phase, unit, total);
156    }
157    fn inc(&mut self, units: u64) {
158        (**self).inc(units);
159    }
160    fn set(&mut self, current: u64) {
161        (**self).set(current);
162    }
163    fn finish(&mut self) {
164        (**self).finish();
165    }
166    fn should_cancel(&self) -> bool {
167        (**self).should_cancel()
168    }
169}
170
171/// A [`ProgressSink`] that discards every update. The default for tests,
172/// `grit-cli`, and any call site that does not want progress output.
173#[derive(Debug, Default, Clone, Copy)]
174pub struct NullProgress;
175
176impl ProgressSink for NullProgress {}
177
178/// A cancellation signal checked by long-running library operations at loop
179/// boundaries. Implementations must be cheap to query.
180pub trait Cancel {
181    /// Returns `true` once the operation should stop as soon as possible.
182    fn is_cancelled(&self) -> bool;
183
184    /// When cancellation is backed by a shared flag (for example [`FlagCancel`]), returns it
185    /// so worker threads can observe the same signal.
186    fn shared_watch(&self) -> Option<std::sync::Arc<std::sync::atomic::AtomicBool>> {
187        None
188    }
189}
190
191/// A [`Cancel`] that never signals cancellation. The default for call sites
192/// that do not support interruption.
193#[derive(Debug, Default, Clone, Copy)]
194pub struct NeverCancel;
195
196impl Cancel for NeverCancel {
197    fn is_cancelled(&self) -> bool {
198        false
199    }
200}
201
202/// Shared flag for tests and embedders that flip cancellation mid-operation.
203#[derive(Debug, Clone)]
204pub struct FlagCancel(pub std::sync::Arc<std::sync::atomic::AtomicBool>);
205
206impl FlagCancel {
207    /// Create a cancel signal that starts unset.
208    #[must_use]
209    pub fn new() -> Self {
210        Self(std::sync::Arc::new(std::sync::atomic::AtomicBool::new(
211            false,
212        )))
213    }
214
215    /// Signal cancellation to every consumer polling this handle.
216    pub fn cancel(&self) {
217        self.0.store(true, std::sync::atomic::Ordering::Relaxed);
218    }
219}
220
221impl Default for FlagCancel {
222    fn default() -> Self {
223        Self::new()
224    }
225}
226
227impl Cancel for FlagCancel {
228    fn is_cancelled(&self) -> bool {
229        self.0.load(std::sync::atomic::Ordering::Relaxed)
230    }
231
232    fn shared_watch(&self) -> Option<std::sync::Arc<std::sync::atomic::AtomicBool>> {
233        Some(std::sync::Arc::clone(&self.0))
234    }
235}
236
237/// Shared [`AtomicBool`](std::sync::atomic::AtomicBool) for cancellation visible across threads.
238///
239/// For [`FlagCancel`], this is the same flag as the handle (so embedders can cancel mid-scan).
240/// For other [`Cancel`] implementations, a private flag is seeded from [`Cancel::is_cancelled`]
241/// at creation time; combine with [`poll_cancel_watch`] on the main thread for progress signals.
242#[must_use]
243pub fn cancel_watch_arc(cancel: &dyn Cancel) -> std::sync::Arc<std::sync::atomic::AtomicBool> {
244    cancel.shared_watch().unwrap_or_else(|| {
245        std::sync::Arc::new(std::sync::atomic::AtomicBool::new(cancel.is_cancelled()))
246    })
247}
248
249/// Returns [`crate::error::Error::Cancelled`] when `watch` was set.
250#[inline]
251pub fn poll_watch(watch: &std::sync::atomic::AtomicBool) -> crate::error::Result<()> {
252    if watch.load(std::sync::atomic::Ordering::Relaxed) {
253        Err(crate::error::Error::Cancelled)
254    } else {
255        Ok(())
256    }
257}
258
259/// Like [`poll_cancel`], but also records cancellation in `watch` for worker threads.
260#[inline]
261pub fn poll_cancel_watch(
262    progress: &mut dyn ProgressSink,
263    cancel: &dyn Cancel,
264    watch: &std::sync::atomic::AtomicBool,
265) -> crate::error::Result<()> {
266    if cancel.is_cancelled() || progress.should_cancel() {
267        watch.store(true, std::sync::atomic::Ordering::Relaxed);
268        return Err(crate::error::Error::Cancelled);
269    }
270    poll_watch(watch)
271}
272
273/// [`ProgressSink`] adapter that mirrors [`ProgressSink::should_cancel`] into `watch`.
274pub struct WatchProgress<'a> {
275    inner: &'a mut dyn ProgressSink,
276    watch: std::sync::Arc<std::sync::atomic::AtomicBool>,
277}
278
279impl<'a> WatchProgress<'a> {
280    /// Wrap `inner` so any cancel request on the sink sets `watch`.
281    #[must_use]
282    pub fn new(
283        inner: &'a mut dyn ProgressSink,
284        watch: std::sync::Arc<std::sync::atomic::AtomicBool>,
285    ) -> Self {
286        Self { inner, watch }
287    }
288
289    fn note_cancel(&self) {
290        if self.inner.should_cancel() {
291            self.watch.store(true, std::sync::atomic::Ordering::Relaxed);
292        }
293    }
294}
295
296impl ProgressSink for WatchProgress<'_> {
297    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
298        self.note_cancel();
299        self.inner.start(phase, total);
300    }
301    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
302        self.note_cancel();
303        self.inner.start_with_unit(phase, unit, total);
304    }
305    fn inc(&mut self, units: u64) {
306        self.note_cancel();
307        self.inner.inc(units);
308    }
309    fn set(&mut self, current: u64) {
310        self.note_cancel();
311        self.inner.set(current);
312    }
313    fn finish(&mut self) {
314        self.note_cancel();
315        self.inner.finish();
316    }
317    fn should_cancel(&self) -> bool {
318        self.inner.should_cancel()
319    }
320}
321
322/// The progress sink, cancel signal and shared watch flag one operation threads through its
323/// helpers, so worker threads and the calling thread stop on the same signal.
324pub(crate) struct ProgressScope<'a> {
325    sink: WatchProgress<'a>,
326    cancel: &'a dyn Cancel,
327    watch: std::sync::Arc<std::sync::atomic::AtomicBool>,
328}
329
330impl<'a> ProgressScope<'a> {
331    pub(crate) fn new(sink: &'a mut dyn ProgressSink, cancel: &'a dyn Cancel) -> Self {
332        let watch = cancel_watch_arc(cancel);
333        Self {
334            sink: WatchProgress::new(sink, std::sync::Arc::clone(&watch)),
335            cancel,
336            watch,
337        }
338    }
339
340    /// Returns [`crate::error::Error::Cancelled`] once either signal fired.
341    pub(crate) fn poll(&mut self) -> crate::error::Result<()> {
342        poll_cancel_watch(&mut self.sink, self.cancel, &self.watch)
343    }
344
345    /// Flag for worker threads; set once [`Self::poll`] observed a cancel.
346    pub(crate) fn watch(&self) -> &std::sync::atomic::AtomicBool {
347        &self.watch
348    }
349
350    pub(crate) fn sink(&mut self) -> &mut dyn ProgressSink {
351        &mut self.sink
352    }
353}
354
355/// [`ProgressSink`] adapter that forwards every update but never asks to stop, for steps that
356/// must run to completion once an operation has published its state.
357pub(crate) struct UncancellableProgress<'a> {
358    inner: &'a mut dyn ProgressSink,
359}
360
361impl<'a> UncancellableProgress<'a> {
362    pub(crate) fn new(inner: &'a mut dyn ProgressSink) -> Self {
363        Self { inner }
364    }
365}
366
367impl ProgressSink for UncancellableProgress<'_> {
368    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
369        self.inner.start(phase, total);
370    }
371    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
372        self.inner.start_with_unit(phase, unit, total);
373    }
374    fn inc(&mut self, units: u64) {
375        self.inner.inc(units);
376    }
377    fn set(&mut self, current: u64) {
378        self.inner.set(current);
379    }
380    fn finish(&mut self) {
381        self.inner.finish();
382    }
383}
384
385/// Returns [`crate::error::Error::Cancelled`] when `cancel` or `progress` request stop.
386#[inline]
387pub fn poll_cancel(
388    progress: &mut dyn ProgressSink,
389    cancel: &dyn Cancel,
390) -> crate::error::Result<()> {
391    if cancel.is_cancelled() || progress.should_cancel() {
392        Err(crate::error::Error::Cancelled)
393    } else {
394        Ok(())
395    }
396}
397
398/// [`ProgressSink`] that stops forwarding once [`Cancel`] or [`ProgressSink::should_cancel`] fires.
399///
400/// Long checkout loops call [`Self::finish_poll`] after apply so callers get [`crate::error::Error::Cancelled`]
401/// even when cancellation happened on an [`ProgressSink::inc`] boundary.
402pub struct CancelProgress<'a> {
403    inner: &'a mut dyn ProgressSink,
404    cancel: &'a dyn Cancel,
405    stopped: std::cell::Cell<bool>,
406}
407
408impl<'a> CancelProgress<'a> {
409    /// Wrap `inner` so every progress update observes `cancel`.
410    pub fn new(inner: &'a mut dyn ProgressSink, cancel: &'a dyn Cancel) -> Self {
411        Self {
412            inner,
413            cancel,
414            stopped: std::cell::Cell::new(false),
415        }
416    }
417
418    /// Whether a cancel signal was observed.
419    #[must_use]
420    pub fn stopped(&self) -> bool {
421        self.stopped.get()
422    }
423
424    /// Returns [`crate::error::Error::Cancelled`] when [`Self::stopped`].
425    pub fn finish_poll(&self) -> crate::error::Result<()> {
426        if self.stopped() {
427            Err(crate::error::Error::Cancelled)
428        } else {
429            Ok(())
430        }
431    }
432
433    fn note_stop(&self) {
434        if self.cancel.is_cancelled() || self.inner.should_cancel() {
435            self.stopped.set(true);
436        }
437    }
438}
439
440impl ProgressSink for CancelProgress<'_> {
441    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
442        self.note_stop();
443        if !self.stopped.get() {
444            self.inner.start(phase, total);
445        }
446    }
447
448    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
449        self.note_stop();
450        if !self.stopped.get() {
451            self.inner.start_with_unit(phase, unit, total);
452        }
453    }
454
455    fn inc(&mut self, units: u64) {
456        self.note_stop();
457        if !self.stopped.get() {
458            self.inner.inc(units);
459        }
460    }
461
462    fn set(&mut self, current: u64) {
463        self.note_stop();
464        if !self.stopped.get() {
465            self.inner.set(current);
466        }
467    }
468
469    fn finish(&mut self) {
470        self.note_stop();
471        if !self.stopped.get() {
472            self.inner.finish();
473        }
474    }
475
476    fn should_cancel(&self) -> bool {
477        self.stopped.get() || self.cancel.is_cancelled() || self.inner.should_cancel()
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use crate::fetch::{NoProgress, Progress};
485
486    /// A sink that records typed phase updates, to prove the trait + forwarding
487    /// impl route calls correctly.
488    #[derive(Default)]
489    struct PhaseRecorder {
490        started: Vec<(ProgressPhase, Option<u64>)>,
491        incs: u64,
492        finished: usize,
493    }
494
495    impl ProgressSink for PhaseRecorder {
496        fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
497            self.started.push((phase, total));
498        }
499        fn inc(&mut self, units: u64) {
500            self.incs += units;
501        }
502        fn finish(&mut self) {
503            self.finished += 1;
504        }
505    }
506
507    fn drive_typed(sink: &mut impl ProgressSink) {
508        sink.start(ProgressPhase::ReceivingObjects, Some(3));
509        sink.inc(1);
510        sink.inc(2);
511        sink.finish();
512    }
513
514    #[test]
515    fn null_progress_is_inert() {
516        drive_typed(&mut NullProgress);
517    }
518
519    #[test]
520    fn no_progress_is_inert() {
521        drive_typed(&mut NoProgress);
522    }
523
524    #[test]
525    fn typed_phase_recorder_roundtrip() {
526        let mut rec = PhaseRecorder::default();
527        drive_typed(&mut &mut rec);
528        assert_eq!(
529            rec.started,
530            vec![(ProgressPhase::ReceivingObjects, Some(3))]
531        );
532        assert_eq!(rec.incs, 3);
533        assert_eq!(rec.finished, 1);
534    }
535
536    #[test]
537    fn progress_supertrait_upcasts() {
538        struct SidebandCap(Vec<u8>);
539
540        impl ProgressSink for SidebandCap {}
541
542        impl Progress for SidebandCap {
543            fn message(&mut self, bytes: &[u8]) {
544                self.0.extend_from_slice(bytes);
545            }
546        }
547
548        fn takes_sink(sink: &mut dyn ProgressSink) {
549            sink.start(ProgressPhase::IndexingObjects, None);
550            sink.finish();
551        }
552
553        let mut cap = SidebandCap(Vec::new());
554        let progress: &mut dyn Progress = &mut cap;
555        takes_sink(progress);
556    }
557
558    #[test]
559    fn never_cancel_never_cancels() {
560        assert!(!NeverCancel.is_cancelled());
561    }
562
563    #[test]
564    fn poll_cancel_returns_typed_error() {
565        let flag = FlagCancel::new();
566        flag.cancel();
567        let mut sink = NullProgress;
568        let err = poll_cancel(&mut sink, &flag).expect_err("cancelled");
569        assert!(matches!(err, crate::error::Error::Cancelled));
570    }
571
572    #[test]
573    fn phase_default_unit() {
574        assert_eq!(
575            ProgressPhase::ReceivingObjects.default_unit(),
576            ProgressUnit::Objects
577        );
578        assert_eq!(
579            ProgressPhase::CheckingOutFiles.default_unit(),
580            ProgressUnit::Files
581        );
582        assert_eq!(
583            ProgressPhase::StagingFiles.default_unit(),
584            ProgressUnit::Files
585        );
586    }
587}