Skip to content

Commit 43ade11

Browse files
committed
Windows support (experimental)
Experimental support for Windows (known issues can be found on the issue tracker). Related to jorisroovers#20 and heavily influenced by jorisroovers#71.
1 parent 3ee281e commit 43ade11

14 files changed

+138
-17
lines changed

.travis.yml

+23
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ matrix:
2020
os: linux
2121
- python: "pypy3.5"
2222
os: linux
23+
24+
# Using 'sh' shim
25+
- python: "2.7"
26+
env: GITLINT_USE_SH_LIB=0
27+
os: linux
28+
- python: "3.7"
29+
env: GITLINT_USE_SH_LIB=0
30+
os: linux
31+
dist: xenial
32+
sudo: true
33+
34+
# Windows
35+
# Travis doesn't support python on windows yet:
36+
# https://travis-ci.community/t/python-support-on-windows
37+
# Unit tests are known to have issues on windows: https://github.com/jorisroovers/gitlint/issues/92
38+
# For now, we just run a few simple sanity tests
39+
# - python: "3.7"
40+
# os: windows
41+
# script:
42+
# - pytest -rw -s gitlint\tests\test_cli.py::CLITests::test_lint
43+
# - gitlint --version
44+
# - gitlint
45+
2346
install:
2447
- "pip install -r requirements.txt"
2548
- "pip install -r test-requirements.txt"

docs/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Great for use as a [commit-msg git hook](#using-gitlint-as-a-commit-msg-hook) or
77
<script type="text/javascript" src="https://asciinema.org/a/30477.js" id="asciicast-30477" async></script>
88

99
!!! note
10-
Gitlint is currently [**not** supported on Windows](https://github.com/jorisroovers/gitlint/issues/20).
10+
**Gitlint support for Windows is still experimental**, and [there are some known issues](https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows).
1111

1212
Also, gitlint is not the only git commit message linter out there, if you are looking for an alternative written in a different language,
1313
have a look at [fit-commit](https://github.com/m1foley/fit-commit) (Ruby),

gitlint/cli.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,6 @@
1414
GIT_CONTEXT_ERROR_CODE = 254 # noqa
1515
CONFIG_ERROR_CODE = 255 # noqa
1616

17-
# We need to make sure we're not on Windows before importing other gitlint modules, as some of these modules use sh
18-
# which will raise an exception when imported on Windows.
19-
if "windows" in platform.system().lower(): # noqa
20-
click.echo("Gitlint currently does not support Windows. Check out " # noqa
21-
"https://github.com/jorisroovers/gitlint/issues/20 for details.", err=True) # noqa
22-
exit(USAGE_ERROR_CODE) # noqa
23-
2417
import gitlint
2518
from gitlint.lint import GitLinter
2619
from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator
@@ -52,6 +45,7 @@ def log_system_info():
5245
LOG.debug("Python version: %s", sys.version)
5346
LOG.debug("Git version: %s", git_version())
5447
LOG.debug("Gitlint version: %s", gitlint.__version__)
48+
LOG.debug("GITLINT_USE_SH_LIB: %s", os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]"))
5549

5650

5751
def build_config( # pylint: disable=too-many-arguments

gitlint/git.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import os
2-
32
import arrow
4-
import sh
3+
4+
from gitlint import shell as sh
55
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
6-
from sh import CommandNotFound, ErrorReturnCode
6+
from gitlint.shell import CommandNotFound, ErrorReturnCode
77

88
from gitlint.utils import ustr, sstr
99

@@ -22,7 +22,6 @@ def __init__(self):
2222

2323
def _git(*command_parts, **kwargs):
2424
""" Convenience function for running git commands. Automatically deals with exceptions and unicode. """
25-
# Special arguments passed to sh: http://amoffat.github.io/sh/special_arguments.html
2625
git_kwargs = {'_tty_out': False}
2726
git_kwargs.update(kwargs)
2827
try:

gitlint/shell.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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)

gitlint/tests/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class BaseTestCase(unittest.TestCase):
3939

4040
SAMPLES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "samples")
4141
EXPECTED_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected")
42+
GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]")
4243

4344
# List of 'git config' side-effects that can be used when mocking calls to git
4445
GIT_CONFIG_SIDE_EFFECTS = [

gitlint/tests/test_cli.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
# python 3.x
2525
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
2626

27-
from sh import CommandNotFound
27+
from gitlint.shell import CommandNotFound
2828

2929
from gitlint.tests.base import BaseTestCase
3030
from gitlint import cli
@@ -306,6 +306,7 @@ def test_debug(self, sh, _):
306306
u"DEBUG: gitlint.cli Python version: {0}".format(sys.version),
307307
u"DEBUG: gitlint.cli Git version: git version 1.2.3",
308308
u"DEBUG: gitlint.cli Gitlint version: {0}".format(__version__),
309+
u"DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {0}".format(self.GITLINT_USE_SH_LIB),
309310
self.get_expected('debug_configuration_output1',
310311
{'config_path': config_path, 'target': os.path.realpath(os.getcwd())}),
311312
u"DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. " +
@@ -395,13 +396,13 @@ def test_config_file_negative(self):
395396
@patch('gitlint.cli.get_stdin_data', return_value=False)
396397
def test_target(self, _):
397398
""" Test for the --target option """
398-
os.environ["LANGUAGE"] = "C"
399+
os.environ["LANGUAGE"] = "C" # Force language to english so we can check for error message
399400
result = self.cli.invoke(cli.cli, ["--target", "/tmp"])
400401
# We expect gitlint to tell us that /tmp is not a git repo (this proves that it takes the target parameter
401402
# into account).
402-
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
403403
expected_path = os.path.realpath("/tmp")
404404
self.assertEqual(result.output, "%s is not a git repository.\n" % expected_path)
405+
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
405406

406407
def test_target_negative(self):
407408
""" Negative test for the --target option """

gitlint/tests/test_git.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# python 3.x
1313
from unittest.mock import patch, call # pylint: disable=no-name-in-module, import-error
1414

15-
from sh import ErrorReturnCode, CommandNotFound
15+
from gitlint.shell import ErrorReturnCode, CommandNotFound
1616

1717
from gitlint.tests.base import BaseTestCase
1818
from gitlint.git import GitContext, GitCommit, GitCommitMessage, GitContextError, \

gitlint/utils.py

+11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return
2+
import platform
23
import sys
4+
import os
35

46
from locale import getpreferredencoding
57

68
DEFAULT_ENCODING = getpreferredencoding() or "UTF-8"
79
LOG_FORMAT = '%(levelname)s: %(name)s %(message)s'
810

11+
# On windows we won't want to use the sh library since it's not supported - instead we'll use our own shell module.
12+
# However, we want to be able to overwrite this behavior for testing using the GITLINT_USE_SH_LIB env var.
13+
PLATFORM_IS_WINDOWS = "windows" in platform.system().lower()
14+
GITLINT_USE_SH_LIB_ENV = os.environ.get('GITLINT_USE_SH_LIB', None)
15+
if GITLINT_USE_SH_LIB_ENV:
16+
USE_SH_LIB = (GITLINT_USE_SH_LIB_ENV == "1")
17+
else:
18+
USE_SH_LIB = not PLATFORM_IS_WINDOWS
19+
920

1021
def ustr(obj):
1122
""" Python 2 and 3 utility method that converts an obj to unicode in python 2 and to a str object in python 3"""

qa/base.py

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ class BaseTestCase(TestCase):
4646
maxDiff = None
4747
tmp_git_repo = None
4848

49+
GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]")
50+
4951
def setUp(self):
5052
self.tmpfiles = []
5153

qa/expected/debug_output1

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ DEBUG: gitlint.cli Platform: {platform}
33
DEBUG: gitlint.cli Python version: {python_version}
44
DEBUG: gitlint.cli Git version: {git_version}
55
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
6+
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
67
DEBUG: gitlint.cli Configuration
78
config-path: {config_path}
89
[GENERAL]

qa/test_config.py

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def test_config_from_file_debug(self):
8686
expected = self.get_expected('debug_output1', {'platform': platform.platform(), 'python_version': sys.version,
8787
'git_version': expected_git_version,
8888
'gitlint_version': expected_gitlint_version,
89+
'GITLINT_USE_SH_LIB': self.GITLINT_USE_SH_LIB,
8990
'config_path': config_path, 'target': self.tmp_git_repo,
9091
'commit_sha': commit_sha, 'commit_date': expected_date})
9192

run_tests.sh

+3-1
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,11 @@ run_stats(){
212212
}
213213

214214
clean(){
215-
echo -n "Cleaning the site, build, dist and all __pycache__directories..."
215+
echo -n "Cleaning the *.pyc, site/, build/, dist/ and all __pycache__ directories..."
216216
find gitlint -type d -name "__pycache__" -exec rm -rf {} \; 2> /dev/null
217217
find qa -type d -name "__pycache__" -exec rm -rf {} \; 2> /dev/null
218+
find gitlint -iname *.pyc -exec rm -rf {} \; 2> /dev/null
219+
find qa -iname *.pyc -exec rm -rf {} \; 2> /dev/null
218220
rm -rf "site" "dist" "build"
219221
echo -e "${GREEN}DONE${NO_COLOR}"
220222
}

setup.py

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io
55
import re
66
import os
7+
import platform
78
import sys
89

910
# There is an issue with building python packages in a shared vagrant directory because of how setuptools works
@@ -98,3 +99,12 @@ def get_version(package):
9899
msg = "\033[31mDEPRECATION: Python 2.6 or below are no longer supported by gitlint or the Python core team." + \
99100
"Please upgrade your Python to a later version.\033[0m"
100101
print(msg)
102+
103+
# Print a red deprecation warning for python 2.6 users
104+
PLATFORM_IS_WINDOWS = "windows" in platform.system().lower()
105+
if PLATFORM_IS_WINDOWS:
106+
msg = "\n\n\n\n\n****************\n" + \
107+
"WARNING: Gitlint support for Windows is still experimental and there are some known issues: " + \
108+
"https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows " + \
109+
"\n*******************"
110+
print(msg)

0 commit comments

Comments
 (0)