2222
2323logger = logging .getLogger (__name__ )
2424
25+ console_encoding = sys .stdout .encoding
26+
27+
28+ def console_to_str (s :bytes )-> str :
29+ """From pypa/pip project, pip.backwardwardcompat. License MIT."""
30+ try :
31+ return s .decode (console_encoding )
32+ except UnicodeDecodeError :
33+ return s .decode ("utf_8" )
34+ except AttributeError :# for tests, #13
35+ return str (s )
36+
2537
2638if t .TYPE_CHECKING :
2739_LoggerAdapter = logging .LoggerAdapter [logging .Logger ]
@@ -78,7 +90,7 @@ def process(
7890class ProgressCallbackProtocol (t .Protocol ):
7991"""Callback to report subprocess communication."""
8092
81- def __call__ (self ,output :t . AnyStr ,timestamp :datetime .datetime )-> None :
93+ def __call__ (self ,output :str ,timestamp :datetime .datetime )-> None :
8294"""Process progress for subprocess communication."""
8395 ...
8496
@@ -182,7 +194,7 @@ def progress_cb(output, timestamp):
182194restore_signals = restore_signals ,
183195start_new_session = start_new_session ,
184196pass_fds = pass_fds ,
185- text = True ,
197+ text = False , # Keep in bytes mode to preserve \r properly
186198encoding = encoding ,
187199errors = errors ,
188200user = user ,
@@ -201,29 +213,36 @@ def progress_cb(output: t.AnyStr, timestamp: datetime.datetime) -> None:
201213sys .stdout .flush ()
202214
203215callback = progress_cb
216+
217+ # Note: When git detects that stderr is not a TTY (e.g., when piped),
218+ # it outputs progress with newlines instead of carriage returns.
219+ # This causes each progress update to appear on a new line.
220+ # To get proper single-line progress updates, git would need to be
221+ # connected to a pseudo-TTY, which would require significant changes
222+ # to how subprocess execution is handled.
223+
204224while code is None :
205225code = proc .poll ()
206226
207227if callback and callable (callback )and proc .stderr is not None :
208- line = str (proc .stderr .read (128 ))
228+ line = console_to_str (proc .stderr .read (128 ))
209229if line :
210230callback (output = line ,timestamp = datetime .datetime .now ())
211231if callback and callable (callback ):
212232callback (output = "\r " ,timestamp = datetime .datetime .now ())
213233
214- lines = (
215- filter (None , (line .strip ()for line in proc .stdout .readlines ()))
216- if proc .stdout is not None
217- else []
218- )
219- all_output = "\n " .join (lines )
220- if code :
221- stderr_lines = (
222- filter (None , (line .strip ()for line in proc .stderr .readlines ()))
223- if proc .stderr is not None
224- else []
234+ if proc .stdout is not None :
235+ lines :t .Iterable [bytes ]= filter (
236+ None , (line .strip ()for line in proc .stdout .readlines ())
237+ )
238+ all_output = console_to_str (b"\n " .join (lines ))
239+ else :
240+ all_output = ""
241+ if code and proc .stderr is not None :
242+ stderr_lines :t .Iterable [bytes ]= filter (
243+ None , (line .strip ()for line in proc .stderr .readlines ())
225244 )
226- all_output = "" .join (stderr_lines )
245+ all_output = console_to_str ( b "" .join (stderr_lines ) )
227246output = "" .join (all_output )
228247if code != 0 and check_returncode :
229248raise exc .CommandError (