From ff65558a1b2c4c3af72585d5a2bde5e0598ea9d7 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sat, 16 Mar 2024 22:30:36 +0800 Subject: [PATCH] Align sideband progress message printing with Git's implementation See https://github.com/git/git/blob/43072b4ca132437f21975ac6acc6b72dc22fd398/sideband.c#L178. --- cli/src/git_util.rs | 70 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/cli/src/git_util.rs b/cli/src/git_util.rs index 41c703a173..5c4e712867 100644 --- a/cli/src/git_util.rs +++ b/cli/src/git_util.rs @@ -14,13 +14,14 @@ //! Git utilities shared by various commands. -use std::io::{Read, Write}; +use std::io::{IsTerminal, Read, Write}; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::Mutex; use std::time::Instant; use std::{error, iter}; +use gix::bstr::ByteSlice; use itertools::Itertools; use jj_lib::git::{self, FailedRefExport, FailedRefExportReason, GitImportStats, RefName}; use jj_lib::git_backend::GitBackend; @@ -155,16 +156,69 @@ pub fn with_remote_git_callbacks( callbacks.progress = progress_callback .as_mut() .map(|x| x as &mut dyn FnMut(&git::Progress)); + // Based on Git's implementation: https://github.com/git/git/blob/43072b4ca132437f21975ac6acc6b72dc22fd398/sideband.c#L178 let mut sideband_progress_callback = None; + let mut scratch = Vec::new(); + let display_prefix = "remote: "; + let suffix = if std::io::stdout().is_terminal() { + "\x1B[K" + } else { + " " + }; if print_sideband_messages { - sideband_progress_callback = Some(|progress: &[u8]| { - let message = String::from_utf8_lossy(progress); - if message.contains("\n") { - for line in message.lines() { - _ = writeln!(ui.lock().unwrap().stderr(), "remote: {line}"); + sideband_progress_callback = Some(|progress_message: &[u8]| { + let mut index = 0; + // Append a suffix to each nonempty line to clear the end of the screen line. + loop { + let Some(i) = progress_message[index..] + .find_char('\n') + .or_else(|| progress_message[index..].find_char('\r')) + .map(|i| index + i) + else { + break; + }; + let line_length = i - index; + + // For message across packet boundary, there would have a nonempty "scratch" + // buffer from last call of this function, and there may have a + // leading CR/LF in "buf". For this case we should add a clear-to-eol suffix to + // clean leftover letters we previously have written on the same line. + if !scratch.is_empty() && line_length == 0 { + scratch.extend_from_slice(suffix.as_bytes()); + } + + if scratch.is_empty() { + scratch.extend_from_slice(display_prefix.as_bytes()); + } + + // Do not add the clear-to-eol suffix to empty lines: + // For progress reporting we may receive a bunch of percentage updates followed + // by '\r' to remain on the same line, and at the end receive a + // single '\n' to move to the next line. We should preserve the final + // status report line by not appending clear-to-eol suffix to this single line + // break. + if line_length > 0 { + scratch.extend_from_slice(&progress_message[index..i]); + scratch.extend_from_slice(suffix.as_bytes()); + } + scratch.extend_from_slice(&progress_message[i..i + 1]); + + _ = write!( + ui.lock().unwrap().stderr(), + "{}", + String::from_utf8_lossy(&scratch) + ); + scratch.clear(); + + index = i + 1; + } + + // Add leftover message to "scratch" buffer to be printed in next call. + if index < progress_message.len() && progress_message[index] != 0 { + if scratch.is_empty() { + scratch.extend_from_slice(display_prefix.as_bytes()); } - } else { - _ = write!(ui.lock().unwrap().stderr(), "remote: {message}"); + scratch.extend_from_slice(&progress_message[index..]); } }); }