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):
182194 restore_signals = restore_signals ,
183195 start_new_session = start_new_session ,
184196 pass_fds = pass_fds ,
185- text = True ,
197+ text = False , # Keep in bytes mode to preserve \r properly
186198 encoding = encoding ,
187199 errors = errors ,
188200 user = user ,
@@ -201,29 +213,36 @@ def progress_cb(output: t.AnyStr, timestamp: datetime.datetime) -> None:
201213 sys .stdout .flush ()
202214
203215 callback = 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+
204224 while code is None :
205225 code = proc .poll ()
206226
207227 if 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 ))
209229 if line :
210230 callback (output = line , timestamp = datetime .datetime .now ())
211231 if callback and callable (callback ):
212232 callback (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 ) )
227246 output = "" .join (all_output )
228247 if code != 0 and check_returncode :
229248 raise exc .CommandError (
0 commit comments