Skip to main content

gix_protocol/
remote_progress.rs

1use bstr::ByteSlice;
2
3/// The information usually found in remote progress messages as sent by a git server during
4/// fetch, clone and push operations.
5#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7pub struct RemoteProgress<'a> {
8    #[cfg_attr(feature = "serde", serde(borrow))]
9    /// The name of the action, like "clone".
10    pub action: &'a bstr::BStr,
11    /// The percentage to indicate progress, between 0 and 100.
12    pub percent: Option<u32>,
13    /// The amount of items already processed.
14    pub step: Option<usize>,
15    /// The maximum expected amount of items. `step` / `max` * 100 = `percent`.
16    pub max: Option<usize>,
17}
18
19impl RemoteProgress<'_> {
20    /// Parse the progress from a typical git progress `line` as sent by the remote.
21    pub fn from_bytes(mut line: &[u8]) -> Option<RemoteProgress<'_>> {
22        parse_progress(&mut line).ok().and_then(|r| {
23            if r.percent.is_none() && r.step.is_none() && r.max.is_none() {
24                None
25            } else {
26                Some(r)
27            }
28        })
29    }
30
31    /// Parse `text`, which is interpreted as error if `is_error` is true, as [`RemoteProgress`] and call the respective
32    /// methods on the given `progress` instance.
33    pub fn translate_to_progress(is_error: bool, text: &[u8], progress: &mut impl gix_features::progress::Progress) {
34        fn progress_name(current: Option<String>, action: &[u8]) -> String {
35            match current {
36                Some(current) => format!(
37                    "{}: {}",
38                    current.split_once(':').map_or(&*current, |x| x.0),
39                    action.as_bstr()
40                ),
41                None => action.as_bstr().to_string(),
42            }
43        }
44        if is_error {
45            // ignore keep-alive packages sent with 'sideband-all'
46            if !text.is_empty() {
47                progress.fail(progress_name(None, text));
48            }
49        } else {
50            match RemoteProgress::from_bytes(text) {
51                Some(RemoteProgress {
52                    action,
53                    percent: _,
54                    step,
55                    max,
56                }) => {
57                    progress.set_name(progress_name(progress.name(), action));
58                    progress.init(max, gix_features::progress::count("objects"));
59                    if let Some(step) = step {
60                        progress.set(step);
61                    }
62                }
63                None => progress.set_name(progress_name(progress.name(), text)),
64            }
65        }
66    }
67}
68
69/// Parse a non-empty prefix of ASCII decimal digits as an unsigned number.
70///
71/// On success, `i` is advanced past the parsed digits and the parsed value is
72/// returned. If there are no digits at the current position, `None` is
73/// returned. If the digit prefix cannot be represented as `usize`, `i` is
74/// advanced anyway to avoid retrying the same input and `None` is returned.
75fn parse_number(i: &mut &[u8]) -> Option<usize> {
76    let len = i.iter().take_while(|b| b.is_ascii_digit()).count();
77    if len == 0 {
78        return None;
79    }
80    let (number, rest) = i.split_at(len);
81    *i = rest;
82    gix_utils::btoi::to_signed(number).ok()
83}
84
85/// Advance `i` to the first ASCII digit in the remaining input.
86///
87/// If no digit is present, `i` is advanced to the end of the input.
88/// If `i` already starts with a digit, it is left unchanged.
89fn skip_until_digit_or_to_end(i: &mut &[u8]) {
90    let pos = i.iter().position(u8::is_ascii_digit).unwrap_or(i.len());
91    *i = &i[pos..];
92}
93
94/// Find and parse the next ASCII decimal number only if it is followed by `%`.
95///
96/// For example, `b" 42% (21/50)"` yields `Some(42)` and advances `i` to
97/// `b" (21/50)"`, while `b" (21/50)"` yields `None` because the next number is
98/// not a percentage. `b" done"` yields `None` with `i` fully consumed, as there
99/// are no digits left to parse.
100///
101/// If the digit prefix cannot be represented as `u32`, it is treated as
102/// absent and `None` is returned with `i` advanced past all consumed bytes.
103fn next_optional_percentage(i: &mut &[u8]) -> Option<u32> {
104    let before = *i;
105    skip_until_digit_or_to_end(i);
106    let number = parse_number(i)?;
107    if let Some(rest) = i.strip_prefix(b"%") {
108        *i = rest;
109        u32::try_from(number).ok()
110    } else {
111        *i = before;
112        None
113    }
114}
115
116/// Find and parse the next ASCII decimal number, if one is present.
117///
118/// For example, `b" (21/50)"` yields `Some(21)` and advances `i` to `b"/50)"`.
119/// Calling it again on that remainder yields `Some(50)` and advances `i` to
120/// `b")"`. If no digit is present, it yields `None` and advances `i` to the
121/// empty suffix.
122///
123/// If the next digit prefix cannot be represented as `usize`, it is treated as
124/// absent and `None` is returned. In that case, `i` is advanced past the digit
125/// prefix because [`parse_number`] consumes it before conversion.
126fn next_optional_number(i: &mut &[u8]) -> Option<usize> {
127    skip_until_digit_or_to_end(i);
128    parse_number(i)
129}
130
131/// Parse a remote progress line with a non-empty action followed by `:`.
132///
133/// The remainder is scanned leniently for the common progress fields emitted by
134/// git servers: an optional percentage, then up to two optional numbers for the
135/// current step and maximum. For example, inputs like
136/// `b"Receiving objects:  42% (21/50)"` and `b"Resolving deltas: 21/50"` can
137/// produce an action plus `percent`, `step`, and `max` values.
138///
139/// `line` is advanced as the fields are found. If parsing succeeds, it points at
140/// the unconsumed suffix after the parsed progress fields. Inputs without a
141/// colon, or with an empty action before the colon, return an error.
142fn parse_progress<'i>(line: &mut &'i [u8]) -> Result<RemoteProgress<'i>, ()> {
143    let action_end = line.iter().position(|b| *b == b':').ok_or(())?;
144    if action_end == 0 {
145        return Err(());
146    }
147    let action = &line[..action_end];
148    *line = &line[action_end..];
149    let percent = next_optional_percentage(line);
150    let step = next_optional_number(line);
151    let max = next_optional_number(line);
152    Ok(RemoteProgress {
153        action: action.into(),
154        percent,
155        step,
156        max,
157    })
158}