| use bstr::ByteSlice; |
| use winnow::{ |
| combinator::{opt, preceded, terminated}, |
| prelude::*, |
| token::take_till, |
| }; |
| |
| /// The information usually found in remote progress messages as sent by a git server during |
| /// fetch, clone and push operations. |
| #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| pub struct RemoteProgress<'a> { |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| /// The name of the action, like "clone". |
| pub action: &'a bstr::BStr, |
| /// The percentage to indicate progress, between 0 and 100. |
| pub percent: Option<u32>, |
| /// The amount of items already processed. |
| pub step: Option<usize>, |
| /// The maximum expected amount of items. `step` / `max` * 100 = `percent`. |
| pub max: Option<usize>, |
| } |
| |
| impl<'a> RemoteProgress<'a> { |
| /// Parse the progress from a typical git progress `line` as sent by the remote. |
| pub fn from_bytes(mut line: &[u8]) -> Option<RemoteProgress<'_>> { |
| parse_progress(&mut line).ok().and_then(|r| { |
| if r.percent.is_none() && r.step.is_none() && r.max.is_none() { |
| None |
| } else { |
| Some(r) |
| } |
| }) |
| } |
| |
| /// Parse `text`, which is interpreted as error if `is_error` is true, as [`RemoteProgress`] and call the respective |
| /// methods on the given `progress` instance. |
| pub fn translate_to_progress(is_error: bool, text: &[u8], progress: &mut impl gix_features::progress::Progress) { |
| fn progress_name(current: Option<String>, action: &[u8]) -> String { |
| match current { |
| Some(current) => format!( |
| "{}: {}", |
| current.split_once(':').map_or(&*current, |x| x.0), |
| action.as_bstr() |
| ), |
| None => action.as_bstr().to_string(), |
| } |
| } |
| if is_error { |
| // ignore keep-alive packages sent with 'sideband-all' |
| if !text.is_empty() { |
| progress.fail(progress_name(None, text)); |
| } |
| } else { |
| match RemoteProgress::from_bytes(text) { |
| Some(RemoteProgress { |
| action, |
| percent: _, |
| step, |
| max, |
| }) => { |
| progress.set_name(progress_name(progress.name(), action)); |
| progress.init(max, gix_features::progress::count("objects")); |
| if let Some(step) = step { |
| progress.set(step); |
| } |
| } |
| None => progress.set_name(progress_name(progress.name(), text)), |
| }; |
| } |
| } |
| } |
| |
| fn parse_number(i: &mut &[u8]) -> PResult<usize, ()> { |
| take_till(0.., |c: u8| !c.is_ascii_digit()) |
| .try_map(gix_utils::btoi::to_signed) |
| .parse_next(i) |
| } |
| |
| fn next_optional_percentage(i: &mut &[u8]) -> PResult<Option<u32>, ()> { |
| opt(terminated( |
| preceded( |
| take_till(0.., |c: u8| c.is_ascii_digit()), |
| parse_number.try_map(u32::try_from), |
| ), |
| b"%", |
| )) |
| .parse_next(i) |
| } |
| |
| fn next_optional_number(i: &mut &[u8]) -> PResult<Option<usize>, ()> { |
| opt(preceded(take_till(0.., |c: u8| c.is_ascii_digit()), parse_number)).parse_next(i) |
| } |
| |
| fn parse_progress<'i>(line: &mut &'i [u8]) -> PResult<RemoteProgress<'i>, ()> { |
| let action = take_till(1.., |c| c == b':').parse_next(line)?; |
| let percent = next_optional_percentage.parse_next(line)?; |
| let step = next_optional_number.parse_next(line)?; |
| let max = next_optional_number.parse_next(line)?; |
| Ok(RemoteProgress { |
| action: action.into(), |
| percent, |
| step, |
| max, |
| }) |
| } |