use std::io::Read;
use std::process::Command;
use crate::core::provisioning_stage::{ProvisioningStage, emit_with_detail};
use crate::core::spawn_disclaim::disclaimed_stderr_piped_spawn;
pub(crate) struct CloneOutcome {
pub success: bool,
pub stderr: String,
}
pub(crate) fn parse_git_progress(line: &str) -> Option<String> {
let line = line.trim();
let line = line.strip_prefix("remote: ").unwrap_or(line);
let (phase, rest) = line.split_once(':')?;
let phase = phase.trim();
if !matches!(
phase,
"Enumerating objects"
| "Counting objects"
| "Compressing objects"
| "Receiving objects"
| "Resolving deltas"
) {
return None;
}
let pct_end = rest.find('%')?;
let pct: u32 = rest[..pct_end].split_whitespace().last()?.parse().ok()?;
Some(format!("{phase}: {pct}%"))
}
pub(crate) fn clone_with_progress(cmd: Command) -> std::io::Result<CloneOutcome> {
let mut spawned = disclaimed_stderr_piped_spawn(cmd)?;
let mut all: Vec<u8> = Vec::new();
let mut segment: Vec<u8> = Vec::new();
let mut last_emit = String::new();
let mut buf = [0u8; 4096];
loop {
let n = spawned.stderr.read(&mut buf)?;
if n == 0 {
break;
}
all.extend_from_slice(&buf[..n]);
for &b in &buf[..n] {
if b == b'\r' || b == b'\n' {
emit_segment(&segment, &mut last_emit);
segment.clear();
} else {
segment.push(b);
}
}
}
emit_segment(&segment, &mut last_emit);
let status = spawned.wait()?;
Ok(CloneOutcome {
success: status.success(),
stderr: String::from_utf8_lossy(&all).into_owned(),
})
}
fn emit_segment(segment: &[u8], last_emit: &mut String) {
if segment.is_empty() {
return;
}
let line = String::from_utf8_lossy(segment);
if let Some(detail) = parse_git_progress(&line)
&& detail != *last_emit
{
emit_with_detail(ProvisioningStage::CloningRepo, Some(&detail));
*last_emit = detail;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_git_progress_extracts_known_phases() {
assert_eq!(
parse_git_progress("Receiving objects: 63% (12285/19500), 5.00 MiB | 2.5 MiB/s"),
Some("Receiving objects: 63%".to_string())
);
assert_eq!(
parse_git_progress("Resolving deltas: 100% (8000/8000), done."),
Some("Resolving deltas: 100%".to_string())
);
assert_eq!(
parse_git_progress("remote: Compressing objects: 7% (1/14)"),
Some("Compressing objects: 7%".to_string())
);
}
#[test]
fn parse_git_progress_ignores_non_progress_lines() {
assert!(parse_git_progress("Cloning into bare repository 'repo.git'...").is_none());
assert!(parse_git_progress("remote: Total 19500 (delta 0), reused 0").is_none());
assert!(parse_git_progress("").is_none());
assert!(parse_git_progress("Receiving objects: (no percent here)").is_none());
}
}