|
20 | 20 | import subprocess |
21 | 21 | import platform |
22 | 22 | from contextlib import contextmanager |
| 23 | +from io import BufferedReader |
| 24 | +from threading import Thread |
| 25 | +from typing import IO, Optional |
23 | 26 |
|
24 | 27 | # 3rd party libs |
25 | 28 | from termcolor import colored # Assume, colorama is already initialized |
26 | 29 | from git import GitCommandError, CheckoutError as OrigCheckoutError, Git |
| 30 | +from git.cmd import Git as GitCmd |
27 | 31 |
|
28 | 32 | # PyGitUp libs |
29 | 33 | from PyGitUp.utils import find |
@@ -135,8 +139,8 @@ def stash(): |
135 | 139 | )) |
136 | 140 | try: |
137 | 141 | 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) |
140 | 144 |
|
141 | 145 | stashed[0] = True |
142 | 146 |
|
@@ -175,77 +179,60 @@ def rebase(self, target_branch): |
175 | 179 | def fetch(self, *args, **kwargs): |
176 | 180 | """ Fetch remote commits. """ |
177 | 181 |
|
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 | | - |
186 | 182 | # Execute command |
187 | 183 | cmd = self.git.fetch(as_process=True, *args, **kwargs) |
188 | 184 |
|
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) |
215 | 186 |
|
216 | 187 | def push(self, *args, **kwargs): |
217 | | - ''' Push commits to remote ''' |
218 | | - stdout = b'' |
219 | | - |
| 188 | + """ Push commits to remote """ |
220 | 189 | # Execute command |
221 | 190 | cmd = self.git.push(as_process=True, *args, **kwargs) |
222 | 191 |
|
223 | | - # Capture output |
224 | | - while True: |
225 | | - output = cmd.stdout.read(1) |
| 192 | + return self.run_cmd(cmd) |
226 | 193 |
|
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"": |
234 | 207 | 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)) |
235 | 219 |
|
236 | 220 | # Wait for the process to quit |
237 | 221 | try: |
| 222 | + stdout_thread.start() |
| 223 | + stderr_thread.start() |
238 | 224 | cmd.wait() |
| 225 | + stdout_thread.join() |
| 226 | + stderr_thread.join() |
239 | 227 | except GitCommandError as error: |
240 | 228 | # Add more meta-information to errors |
241 | 229 | message = "'{}' returned exit status {}".format( |
242 | 230 | ' '.join(str(c) for c in error.command), |
243 | 231 | error.status |
244 | 232 | ) |
245 | 233 |
|
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() |
249 | 236 |
|
250 | 237 | def config(self, key): |
251 | 238 | """ Return `git config key` output or None. """ |
|
0 commit comments