## Summary - Reuse the existing Zed clone activity indicator, which previously showed only a static `Cloning <URL>` message. - Add filesystem job updates so the existing indicator can change while a job is running. - Stream exact `git clone --progress` messages into that indicator without maintaining a phase allowlist. - Preserve the final Git stderr message when cloning fails. https://github.com/user-attachments/assets/a9b8cbfd-b87f-4d83-a24b-992eb1c4de1f ## Testing - `cargo test -p fs git_clone_progress::tests -- --nocapture` - `CCACHE_DISABLE=1 ./script/clippy -p fs -p activity_indicator` - Manually cloned a repository and verified live Git progress in the activity indicator. Release Notes: - Improved repository cloning by showing live Git progress in the activity indicator.
142 lines
3.8 KiB
Rust
142 lines
3.8 KiB
Rust
use futures::AsyncRead;
|
|
use smol::io::AsyncReadExt;
|
|
use std::io;
|
|
|
|
pub(crate) async fn read(
|
|
mut stderr: impl AsyncRead + Unpin,
|
|
mut on_progress: impl FnMut(String),
|
|
) -> io::Result<Vec<u8>> {
|
|
let mut stderr_output = Vec::new();
|
|
let mut read_buffer = [0; 8192];
|
|
let mut progress = GitCloneProgress::new();
|
|
|
|
loop {
|
|
let bytes_read = stderr.read(&mut read_buffer).await?;
|
|
if bytes_read == 0 {
|
|
break;
|
|
}
|
|
let bytes = &read_buffer[..bytes_read];
|
|
stderr_output.extend_from_slice(bytes);
|
|
progress.push(bytes, &mut on_progress);
|
|
}
|
|
progress.finish(on_progress);
|
|
Ok(stderr_output)
|
|
}
|
|
|
|
pub(crate) fn failure_message(stderr: &[u8]) -> String {
|
|
String::from_utf8_lossy(stderr)
|
|
.split(['\r', '\n'])
|
|
.rev()
|
|
.map(str::trim)
|
|
.find(|line| !line.is_empty())
|
|
.unwrap_or("unknown error")
|
|
.to_owned()
|
|
}
|
|
|
|
struct GitCloneProgress {
|
|
pending: Vec<u8>,
|
|
last_message: Option<String>,
|
|
}
|
|
|
|
impl GitCloneProgress {
|
|
fn new() -> Self {
|
|
Self {
|
|
pending: Vec::new(),
|
|
last_message: None,
|
|
}
|
|
}
|
|
|
|
fn push(&mut self, bytes: &[u8], mut on_progress: impl FnMut(String)) {
|
|
for byte in bytes {
|
|
if matches!(*byte, b'\r' | b'\n') {
|
|
self.flush(&mut on_progress);
|
|
} else {
|
|
self.pending.push(*byte);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn finish(&mut self, mut on_progress: impl FnMut(String)) {
|
|
self.flush(&mut on_progress);
|
|
}
|
|
|
|
fn flush(&mut self, on_progress: &mut impl FnMut(String)) {
|
|
if self.pending.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let message = String::from_utf8_lossy(&self.pending).trim().to_owned();
|
|
if !message.is_empty() && self.last_message.as_ref() != Some(&message) {
|
|
self.last_message = Some(message.clone());
|
|
on_progress(message);
|
|
}
|
|
self.pending.clear();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parses_fragmented_progress_and_suppresses_duplicates() {
|
|
let mut progress = GitCloneProgress::new();
|
|
let mut messages = Vec::new();
|
|
|
|
progress.push(
|
|
b"remote: Enumerating objects: 100, done.\rremote: Count",
|
|
|message| messages.push(message),
|
|
);
|
|
progress.push(
|
|
b"ing objects: 42% (42/100)\rReceiving objects: 100% (100/100)\rReceiving objects: 100% (100/100)\n",
|
|
|message| messages.push(message),
|
|
);
|
|
progress.push(b"Updating files: 75% (3/4)", |message| {
|
|
messages.push(message)
|
|
});
|
|
progress.finish(|message| messages.push(message));
|
|
|
|
assert_eq!(
|
|
messages,
|
|
[
|
|
"remote: Enumerating objects: 100, done.",
|
|
"remote: Counting objects: 42% (42/100)",
|
|
"Receiving objects: 100% (100/100)",
|
|
"Updating files: 75% (3/4)",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn forwards_unrecognized_git_output() {
|
|
let mut progress = GitCloneProgress::new();
|
|
let mut messages = Vec::new();
|
|
|
|
progress.push(b"Cloning into 'repository'...\n", |message| {
|
|
messages.push(message)
|
|
});
|
|
progress.push(b"A future Git progress phase: 12%\r", |message| {
|
|
messages.push(message)
|
|
});
|
|
|
|
assert_eq!(
|
|
messages,
|
|
[
|
|
"Cloning into 'repository'...",
|
|
"A future Git progress phase: 12%"
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_the_last_failure_line() {
|
|
assert_eq!(
|
|
failure_message(
|
|
b"Receiving objects: 42% (42/100)\rfatal: unable to access repository\n"
|
|
),
|
|
"fatal: unable to access repository"
|
|
);
|
|
assert_eq!(failure_message(b""), "unknown error");
|
|
}
|
|
}
|