gix_protocol/
remote_progress.rs

1use bstr::ByteSlice;
2use winnow::{
3    combinator::{opt, preceded, terminated},
4    prelude::*,
5    token::take_till,
6};
7
8/// The information usually found in remote progress messages as sent by a git server during
9/// fetch, clone and push operations.
10#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct RemoteProgress<'a> {
13    #[cfg_attr(feature = "serde", serde(borrow))]
14    /// The name of the action, like "clone".
15    pub action: &'a bstr::BStr,
16    /// The percentage to indicate progress, between 0 and 100.
17    pub percent: Option<u32>,
18    /// The amount of items already processed.
19    pub step: Option<usize>,
20    /// The maximum expected amount of items. `step` / `max` * 100 = `percent`.
21    pub max: Option<usize>,
22}
23
24impl RemoteProgress<'_> {
25    /// Parse the progress from a typical git progress `line` as sent by the remote.
26    pub fn from_bytes(mut line: &[u8]) -> Option<RemoteProgress<'_>> {
27        parse_progress(&mut line).ok().and_then(|r| {
28            if r.percent.is_none() && r.step.is_none() && r.max.is_none() {
29                None
30            } else {
31                Some(r)
32            }
33        })
34    }
35
36    /// Parse `text`, which is interpreted as error if `is_error` is true, as [`RemoteProgress`] and call the respective
37    /// methods on the given `progress` instance.
38    pub fn translate_to_progress(is_error: bool, text: &[u8], progress: &mut impl gix_features::progress::Progress) {
39        fn progress_name(current: Option<String>, action: &[u8]) -> String {
40            match current {
41                Some(current) => format!(
42                    "{}: {}",
43                    current.split_once(':').map_or(&*current, |x| x.0),
44                    action.as_bstr()
45                ),
46                None => action.as_bstr().to_string(),
47            }
48        }
49        if is_error {
50            // ignore keep-alive packages sent with 'sideband-all'
51            if !text.is_empty() {
52                progress.fail(progress_name(None, text));
53            }
54        } else {
55            match RemoteProgress::from_bytes(text) {
56                Some(RemoteProgress {
57                    action,
58                    percent: _,
59                    step,
60                    max,
61                }) => {
62                    progress.set_name(progress_name(progress.name(), action));
63                    progress.init(max, gix_features::progress::count("objects"));
64                    if let Some(step) = step {
65                        progress.set(step);
66                    }
67                }
68                None => progress.set_name(progress_name(progress.name(), text)),
69            };
70        }
71    }
72}
73
74fn parse_number(i: &mut &[u8]) -> PResult<usize, ()> {
75    take_till(0.., |c: u8| !c.is_ascii_digit())
76        .try_map(gix_utils::btoi::to_signed)
77        .parse_next(i)
78}
79
80fn next_optional_percentage(i: &mut &[u8]) -> PResult<Option<u32>, ()> {
81    opt(terminated(
82        preceded(
83            take_till(0.., |c: u8| c.is_ascii_digit()),
84            parse_number.try_map(u32::try_from),
85        ),
86        b"%",
87    ))
88    .parse_next(i)
89}
90
91fn next_optional_number(i: &mut &[u8]) -> PResult<Option<usize>, ()> {
92    opt(preceded(take_till(0.., |c: u8| c.is_ascii_digit()), parse_number)).parse_next(i)
93}
94
95fn parse_progress<'i>(line: &mut &'i [u8]) -> PResult<RemoteProgress<'i>, ()> {
96    let action = take_till(1.., |c| c == b':').parse_next(line)?;
97    let percent = next_optional_percentage.parse_next(line)?;
98    let step = next_optional_number.parse_next(line)?;
99    let max = next_optional_number.parse_next(line)?;
100    Ok(RemoteProgress {
101        action: action.into(),
102        percent,
103        step,
104        max,
105    })
106}