22
22
23
23
logger = logging .getLogger (__name__ )
24
24
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
+
25
37
26
38
if t .TYPE_CHECKING :
27
39
_LoggerAdapter = logging .LoggerAdapter [logging .Logger ]
@@ -78,7 +90,7 @@ def process(
78
90
class ProgressCallbackProtocol (t .Protocol ):
79
91
"""Callback to report subprocess communication."""
80
92
81
- def __call__ (self , output : t . AnyStr , timestamp : datetime .datetime ) -> None :
93
+ def __call__ (self , output : str , timestamp : datetime .datetime ) -> None :
82
94
"""Process progress for subprocess communication."""
83
95
...
84
96
@@ -182,7 +194,7 @@ def progress_cb(output, timestamp):
182
194
restore_signals = restore_signals ,
183
195
start_new_session = start_new_session ,
184
196
pass_fds = pass_fds ,
185
- text = True ,
197
+ text = False , # Keep in bytes mode to preserve \r properly
186
198
encoding = encoding ,
187
199
errors = errors ,
188
200
user = user ,
@@ -201,29 +213,36 @@ def progress_cb(output: t.AnyStr, timestamp: datetime.datetime) -> None:
201
213
sys .stdout .flush ()
202
214
203
215
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
+
204
224
while code is None :
205
225
code = proc .poll ()
206
226
207
227
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 ))
209
229
if line :
210
230
callback (output = line , timestamp = datetime .datetime .now ())
211
231
if callback and callable (callback ):
212
232
callback (output = "\r " , timestamp = datetime .datetime .now ())
213
233
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 ())
225
244
)
226
- all_output = "" .join (stderr_lines )
245
+ all_output = console_to_str ( b "" .join (stderr_lines ) )
227
246
output = "" .join (all_output )
228
247
if code != 0 and check_returncode :
229
248
raise exc .CommandError (
0 commit comments