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