Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ea547ba

Browse files
authored
refactor: use threads to read git stdout/stderr (#132)
1 parent 0aa4378 commit ea547ba

3 files changed

Lines changed: 89 additions & 53 deletions

File tree

PyGitUp/git_wrapper.py

Lines changed: 39 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
2020
import subprocess
2121
import platform
2222
from contextlib import contextmanager
23+
from io import BufferedReader
24+
from threading import Thread
25+
from typing import IO, Optional
2326

2427
# 3rd party libs
2528
from termcolor import colored # Assume, colorama is already initialized
2629
from git import GitCommandError, CheckoutError as OrigCheckoutError, Git
30+
from git.cmd import Git as GitCmd
2731

2832
# PyGitUp libs
2933
from PyGitUp.utils import find
@@ -135,8 +139,8 @@ def stash():
135139
))
136140
try:
137141
self._run('stash')
138-
except GitError as e:
139-
raise StashError(stderr=e.stderr, stdout=e.stdout)
142+
except GitError as git_error:
143+
raise StashError(stderr=git_error.stderr, stdout=git_error.stdout)
140144

141145
stashed[0] = True
142146

@@ -175,77 +179,60 @@ def rebase(self, target_branch):
175179
def fetch(self, *args, **kwargs):
176180
""" Fetch remote commits. """
177181

178-
# Unlike the other git commands, we want to output `git fetch`'s
179-
# output in real time. Therefore we use a different implementation
180-
# from `GitWrapper._run` which buffers all output.
181-
# In theory this may deadlock if `git fetch` prints more than 8 KB
182-
# to stderr which is here assumed to not happen in day-to-day use.
183-
184-
stdout = b''
185-
186182
# Execute command
187183
cmd = self.git.fetch(as_process=True, *args, **kwargs)
188184

189-
# Capture output
190-
while True:
191-
output = cmd.stdout.read(1)
192-
193-
sys.stdout.write(output.decode('utf-8'))
194-
sys.stdout.flush()
195-
196-
stdout += output
197-
198-
# Check for EOF
199-
if output == b"":
200-
break
201-
202-
# Wait for the process to quit
203-
try:
204-
cmd.wait()
205-
except GitCommandError as error:
206-
# Add more meta-information to errors
207-
message = "'{}' returned exit status {}".format(
208-
' '.join(str(c) for c in error.command),
209-
error.status
210-
)
211-
212-
raise GitError(message, stderr=error.stderr, stdout=stdout)
213-
214-
return stdout.strip()
185+
return self.run_cmd(cmd)
215186

216187
def push(self, *args, **kwargs):
217-
''' Push commits to remote '''
218-
stdout = b''
219-
188+
""" Push commits to remote """
220189
# Execute command
221190
cmd = self.git.push(as_process=True, *args, **kwargs)
222191

223-
# Capture output
224-
while True:
225-
output = cmd.stdout.read(1)
192+
return self.run_cmd(cmd)
226193

227-
sys.stdout.write(output.decode('utf-8'))
228-
sys.stdout.flush()
229-
230-
stdout += output
231-
232-
# Check for EOF
233-
if output == b"":
194+
@staticmethod
195+
def stream_reader(input_stream: BufferedReader, output_stream: Optional[IO], result_list: list) -> None:
196+
"""
197+
Helper method to read from a stream and write to another stream.
198+
"""
199+
captured_bytes = b""
200+
while True:
201+
read_byte = input_stream.read(1)
202+
captured_bytes += read_byte
203+
if output_stream is not None:
204+
output_stream.write(read_byte.decode('utf-8'))
205+
output_stream.flush()
206+
if read_byte == b"":
234207
break
208+
result_list.append(captured_bytes)
209+
210+
@staticmethod
211+
def run_cmd(cmd: GitCmd.AutoInterrupt) -> bytes:
212+
""" Run a command and return stdout. """
213+
std_outs = []
214+
std_errs = []
215+
stdout_thread = Thread(target=GitWrapper.stream_reader,
216+
args=(cmd.stdout, sys.stdout, std_outs))
217+
stderr_thread = Thread(target=GitWrapper.stream_reader,
218+
args=(cmd.stderr, None, std_errs))
235219

236220
# Wait for the process to quit
237221
try:
222+
stdout_thread.start()
223+
stderr_thread.start()
238224
cmd.wait()
225+
stdout_thread.join()
226+
stderr_thread.join()
239227
except GitCommandError as error:
240228
# Add more meta-information to errors
241229
message = "'{}' returned exit status {}".format(
242230
' '.join(str(c) for c in error.command),
243231
error.status
244232
)
245233

246-
raise GitError(message, stderr=error.stderr, stdout=stdout)
247-
248-
return stdout.strip()
234+
raise GitError(message, stderr=error.stderr, stdout=std_outs[0])
235+
return std_outs[0].strip()
249236

250237
def config(self, key):
251238
""" Return `git config key` output or None. """

PyGitUp/tests/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ def update_file(repo, commit_message='', counter=[0], filename=testfile_name):
6565

6666
return path_file
6767

68+
6869
def mkrepo(path):
6970
"""
70-
Make a repository in 'path', create the the dir, if it doesn't exist.
71+
Make a repository in 'path', create the dir, if it doesn't exist.
7172
"""
7273
return Repo.init(path)
7374

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# System imports
2+
from os import sep, chdir
3+
from os.path import join
4+
import io
5+
6+
from git import *
7+
8+
from PyGitUp.tests import basepath, init_master
9+
10+
TEST_NAME = 'fetch-large-output'
11+
REPO_PATH = join(basepath, TEST_NAME + sep)
12+
13+
14+
def setup():
15+
master_path, master = init_master(TEST_NAME)
16+
17+
# Prepare master repo
18+
master.git.checkout(b=TEST_NAME)
19+
20+
# Clone to test repo
21+
path = join(basepath, TEST_NAME)
22+
23+
master.clone(path, b=TEST_NAME)
24+
repo = Repo(path, odbt=GitCmdObjectDB)
25+
26+
assert repo.working_dir == path
27+
28+
# Generate lots of branches
29+
total_branch_name_bytes = 0
30+
for i in range(0, 1500):
31+
branch_name = 'branch-name-%d' % i
32+
total_branch_name_bytes += len(branch_name)
33+
master.git.checkout(b=branch_name)
34+
35+
36+
def test_fetch_large_output():
37+
""" Run 'git up' with a fetch that outputs lots of data """
38+
# Arrange
39+
chdir(REPO_PATH)
40+
from PyGitUp.gitup import GitUp
41+
gitup = GitUp(testing=True)
42+
43+
# Act
44+
gitup.run()
45+
46+
# Assert
47+
assert len(gitup.states) == 1
48+
assert gitup.states[0] == 'up to date'

0 commit comments

Comments
 (0)