Skip to content

Commit 80eaf40

Browse files
committed
fix(run) Fix live / streaming / flushing of output
Live updates were broken, e.g. flushing of output during `git clone`
1 parent fdf8992 commit 80eaf40

File tree

1 file changed

+34
-15
lines changed

1 file changed

+34
-15
lines changed

src/libvcs/_internal/run.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@
2222

2323
logger = 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

2638
if t.TYPE_CHECKING:
2739
_LoggerAdapter = logging.LoggerAdapter[logging.Logger]
@@ -78,7 +90,7 @@ def process(
7890
class 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

Comments
 (0)