|
| 1 | + |
| 2 | +""" |
| 3 | +This module implements a shim for the 'sh' library, mainly for use on Windows (sh is not supported on Windows). |
| 4 | +We might consider removing the 'sh' dependency alltogether in the future, but 'sh' does provide a few |
| 5 | +capabilities wrt dealing with more edge-case environments on *nix systems that might be useful. |
| 6 | +""" |
| 7 | + |
| 8 | +import subprocess |
| 9 | +import sys |
| 10 | +from gitlint.utils import ustr, USE_SH_LIB |
| 11 | + |
| 12 | +if USE_SH_LIB: |
| 13 | + from sh import git # pylint: disable=unused-import |
| 14 | + # import exceptions separately, this makes it a little easier to mock them out in the unit tests |
| 15 | + from sh import CommandNotFound, ErrorReturnCode |
| 16 | +else: |
| 17 | + |
| 18 | + class CommandNotFound(Exception): |
| 19 | + """ Exception indicating a command was not found during execution """ |
| 20 | + pass |
| 21 | + |
| 22 | + class ShResult(object): |
| 23 | + """ Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using |
| 24 | + the builtin subprocess. module """ |
| 25 | + |
| 26 | + def __init__(self, full_cmd, stdout, stderr='', exitcode=0): |
| 27 | + self.full_cmd = full_cmd |
| 28 | + self.stdout = stdout |
| 29 | + self.stderr = stderr |
| 30 | + self.exit_code = exitcode |
| 31 | + |
| 32 | + def __str__(self): |
| 33 | + return self.stdout |
| 34 | + |
| 35 | + class ErrorReturnCode(ShResult, Exception): |
| 36 | + """ ShResult subclass for unexpected results (acts as an exception). """ |
| 37 | + pass |
| 38 | + |
| 39 | + def git(*command_parts, **kwargs): |
| 40 | + """ Git shell wrapper. |
| 41 | + Implemented as separate function here, so we can do a 'sh' style imports: |
| 42 | + `from shell import git` |
| 43 | + """ |
| 44 | + args = ['git'] + list(command_parts) |
| 45 | + return _exec(*args, **kwargs) |
| 46 | + |
| 47 | + def _exec(*args, **kwargs): |
| 48 | + if sys.version_info[0] == 2: |
| 49 | + no_command_error = OSError # noqa pylint: disable=undefined-variable,invalid-name |
| 50 | + else: |
| 51 | + no_command_error = FileNotFoundError # noqa pylint: disable=undefined-variable |
| 52 | + |
| 53 | + pipe = subprocess.PIPE |
| 54 | + popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs['_tty_out']} |
| 55 | + if '_cwd' in kwargs: |
| 56 | + popen_kwargs['cwd'] = kwargs['_cwd'] |
| 57 | + |
| 58 | + try: |
| 59 | + p = subprocess.Popen(args, **popen_kwargs) |
| 60 | + result = p.communicate() |
| 61 | + except no_command_error: |
| 62 | + raise CommandNotFound |
| 63 | + |
| 64 | + exit_code = p.returncode |
| 65 | + stdout = ustr(result[0]) |
| 66 | + stderr = result[1] # 'sh' does not decode the stderr bytes to unicode |
| 67 | + full_cmd = '' if args is None else ' '.join(args) |
| 68 | + |
| 69 | + # If not _ok_code is specified, then only a 0 exit code is allowed |
| 70 | + ok_exit_codes = kwargs.get('_ok_code', [0]) |
| 71 | + |
| 72 | + if exit_code in ok_exit_codes: |
| 73 | + return ShResult(full_cmd, stdout, stderr, exit_code) |
| 74 | + |
| 75 | + # Unexpected error code => raise ErrorReturnCode |
| 76 | + raise ErrorReturnCode(full_cmd, stdout, stderr, p.returncode) |
0 commit comments