Skip to content

Commit

Permalink
Windows support (experimental)
Browse files Browse the repository at this point in the history
Experimental support for Windows (known issues can be found on the issue
tracker).

Related to #20 and heavily influenced by #71.
  • Loading branch information
jorisroovers committed Jul 8, 2019
1 parent 3ee281e commit 20ac439
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 17 deletions.
23 changes: 23 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@ matrix:
os: linux
- python: "pypy3.5"
os: linux

# Using 'sh' shim
- python: "2.7"
env: GITLINT_USE_SH_LIB=0
os: linux
- python: "3.7"
env: GITLINT_USE_SH_LIB=0
os: linux
dist: xenial
sudo: true

# Windows
# Travis doesn't support python on windows yet:
# https://travis-ci.community/t/python-support-on-windows
# Unit tests are known to have issues on windows: https://github.com/jorisroovers/gitlint/issues/92
# For now, we just run a few simple sanity tests
# - python: "3.7"
# os: windows
# script:
# - pytest -rw -s gitlint\tests\test_cli.py::CLITests::test_lint
# - gitlint --version
# - gitlint

install:
- "pip install -r requirements.txt"
- "pip install -r test-requirements.txt"
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Great for use as a [commit-msg git hook](#using-gitlint-as-a-commit-msg-hook) or
<script type="text/javascript" src="https://asciinema.org/a/30477.js" id="asciicast-30477" async></script>

!!! note
Gitlint is currently [**not** supported on Windows](https://github.com/jorisroovers/gitlint/issues/20).
**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).

Also, gitlint is not the only git commit message linter out there, if you are looking for an alternative written in a different language,
have a look at [fit-commit](https://github.com/m1foley/fit-commit) (Ruby),
Expand Down
8 changes: 1 addition & 7 deletions gitlint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@
GIT_CONTEXT_ERROR_CODE = 254 # noqa
CONFIG_ERROR_CODE = 255 # noqa

# We need to make sure we're not on Windows before importing other gitlint modules, as some of these modules use sh
# which will raise an exception when imported on Windows.
if "windows" in platform.system().lower(): # noqa
click.echo("Gitlint currently does not support Windows. Check out " # noqa
"https://github.com/jorisroovers/gitlint/issues/20 for details.", err=True) # noqa
exit(USAGE_ERROR_CODE) # noqa

import gitlint
from gitlint.lint import GitLinter
from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator
Expand Down Expand Up @@ -52,6 +45,7 @@ def log_system_info():
LOG.debug("Python version: %s", sys.version)
LOG.debug("Git version: %s", git_version())
LOG.debug("Gitlint version: %s", gitlint.__version__)
LOG.debug("GITLINT_USE_SH_LIB: %s", os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]"))


def build_config( # pylint: disable=too-many-arguments
Expand Down
7 changes: 3 additions & 4 deletions gitlint/git.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import os

import arrow
import sh

from gitlint import shell as sh
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
from sh import CommandNotFound, ErrorReturnCode
from gitlint.shell import CommandNotFound, ErrorReturnCode

from gitlint.utils import ustr, sstr

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

def _git(*command_parts, **kwargs):
""" Convenience function for running git commands. Automatically deals with exceptions and unicode. """
# Special arguments passed to sh: http://amoffat.github.io/sh/special_arguments.html
git_kwargs = {'_tty_out': False}
git_kwargs.update(kwargs)
try:
Expand Down
76 changes: 76 additions & 0 deletions gitlint/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

"""
This module implements a shim for the 'sh' library, mainly for use on Windows (sh is not supported on Windows).
We might consider removing the 'sh' dependency alltogether in the future, but 'sh' does provide a few
capabilities wrt dealing with more edge-case environments on *nix systems that might be useful.
"""

import subprocess
import sys
from gitlint.utils import ustr, USE_SH_LIB

if USE_SH_LIB:
from sh import git # pylint: disable=unused-import
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
from sh import CommandNotFound, ErrorReturnCode
else:

class CommandNotFound(Exception):
""" Exception indicating a command was not found during execution """
pass

class ShResult(object):
""" Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using
the builtin subprocess. module """

def __init__(self, full_cmd, stdout, stderr='', exitcode=0):
self.full_cmd = full_cmd
self.stdout = stdout
self.stderr = stderr
self.exit_code = exitcode

def __str__(self):
return self.stdout

class ErrorReturnCode(ShResult, Exception):
""" ShResult subclass for unexpected results (acts as an exception). """
pass

def git(*command_parts, **kwargs):
""" Git shell wrapper.
Implemented as separate function here, so we can do a 'sh' style imports:
`from shell import git`
"""
args = ['git'] + list(command_parts)
return _exec(*args, **kwargs)

def _exec(*args, **kwargs):
if sys.version_info[0] == 2:
no_command_error = OSError # noqa pylint: disable=undefined-variable,invalid-name
else:
no_command_error = FileNotFoundError # noqa pylint: disable=undefined-variable

pipe = subprocess.PIPE
popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs['_tty_out']}
if '_cwd' in kwargs:
popen_kwargs['cwd'] = kwargs['_cwd']

try:
p = subprocess.Popen(args, **popen_kwargs)
result = p.communicate()
except no_command_error:
raise CommandNotFound

exit_code = p.returncode
stdout = ustr(result[0])
stderr = result[1] # 'sh' does not decode the stderr bytes to unicode
full_cmd = '' if args is None else ' '.join(args)

# If not _ok_code is specified, then only a 0 exit code is allowed
ok_exit_codes = kwargs.get('_ok_code', [0])

if exit_code in ok_exit_codes:
return ShResult(full_cmd, stdout, stderr, exit_code)

# Unexpected error code => raise ErrorReturnCode
raise ErrorReturnCode(full_cmd, stdout, stderr, p.returncode)
1 change: 1 addition & 0 deletions gitlint/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class BaseTestCase(unittest.TestCase):

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

# List of 'git config' side-effects that can be used when mocking calls to git
GIT_CONFIG_SIDE_EFFECTS = [
Expand Down
7 changes: 4 additions & 3 deletions gitlint/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error

from sh import CommandNotFound
from gitlint.shell import CommandNotFound

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

def test_target_negative(self):
""" Negative test for the --target option """
Expand Down
2 changes: 1 addition & 1 deletion gitlint/tests/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# python 3.x
from unittest.mock import patch, call # pylint: disable=no-name-in-module, import-error

from sh import ErrorReturnCode, CommandNotFound
from gitlint.shell import ErrorReturnCode, CommandNotFound

from gitlint.tests.base import BaseTestCase
from gitlint.git import GitContext, GitCommit, GitCommitMessage, GitContextError, \
Expand Down
11 changes: 11 additions & 0 deletions gitlint/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return
import platform
import sys
import os

from locale import getpreferredencoding

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

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


def ustr(obj):
""" Python 2 and 3 utility method that converts an obj to unicode in python 2 and to a str object in python 3"""
Expand Down
2 changes: 2 additions & 0 deletions qa/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class BaseTestCase(TestCase):
maxDiff = None
tmp_git_repo = None

GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]")

def setUp(self):
self.tmpfiles = []

Expand Down
1 change: 1 addition & 0 deletions qa/expected/debug_output1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ DEBUG: gitlint.cli Platform: {platform}
DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: {git_version}
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
DEBUG: gitlint.cli Configuration
config-path: {config_path}
[GENERAL]
Expand Down
1 change: 1 addition & 0 deletions qa/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def test_config_from_file_debug(self):
expected = self.get_expected('debug_output1', {'platform': platform.platform(), 'python_version': sys.version,
'git_version': expected_git_version,
'gitlint_version': expected_gitlint_version,
'GITLINT_USE_SH_LIB': self.GITLINT_USE_SH_LIB,
'config_path': config_path, 'target': self.tmp_git_repo,
'commit_sha': commit_sha, 'commit_date': expected_date})

Expand Down
4 changes: 3 additions & 1 deletion run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,11 @@ run_stats(){
}

clean(){
echo -n "Cleaning the site, build, dist and all __pycache__directories..."
echo -n "Cleaning the *.pyc, site/, build/, dist/ and all __pycache__ directories..."
find gitlint -type d -name "__pycache__" -exec rm -rf {} \; 2> /dev/null
find qa -type d -name "__pycache__" -exec rm -rf {} \; 2> /dev/null
find gitlint -iname *.pyc -exec rm -rf {} \; 2> /dev/null
find qa -iname *.pyc -exec rm -rf {} \; 2> /dev/null
rm -rf "site" "dist" "build"
echo -e "${GREEN}DONE${NO_COLOR}"
}
Expand Down
10 changes: 10 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io
import re
import os
import platform
import sys

# There is an issue with building python packages in a shared vagrant directory because of how setuptools works
Expand Down Expand Up @@ -98,3 +99,12 @@ def get_version(package):
msg = "\033[31mDEPRECATION: Python 2.6 or below are no longer supported by gitlint or the Python core team." + \
"Please upgrade your Python to a later version.\033[0m"
print(msg)

# Print a red deprecation warning for python 2.6 users
PLATFORM_IS_WINDOWS = "windows" in platform.system().lower()
if PLATFORM_IS_WINDOWS:
msg = "\n\n\n\n\n****************\n" + \
"WARNING: 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 " + \
"\n*******************"
print(msg)

0 comments on commit 20ac439

Please sign in to comment.