Compare commits
23 Commits
Author | SHA1 | Date |
---|---|---|
Elf M. Sternberg | e86fe1ff07 | |
Ken Sternberg | 41d61476e1 | |
Elf M. Sternberg | ef2a5c579b | |
Tino de Bruijn | b485697742 | |
Ken Sternberg | 1e26d46d17 | |
Ken Sternberg | e5a8f93630 | |
Ken Sternberg | 09dcb1a050 | |
Ken Sternberg | e11655e3b9 | |
Elf M. Sternberg | 5cc8770ac3 | |
Elf M. Sternberg | 8fec735719 | |
Elf M. Sternberg | a8451c83bf | |
Elf M. Sternberg | 1a942e6b1c | |
Elf M. Sternberg | 9b40abe4ae | |
Elf M. Sternberg | 6e09df1427 | |
Elf M. Sternberg | f70aea7ec3 | |
Elf M. Sternberg | 3461709ebf | |
Elf M. Sternberg | 74d217d6fe | |
Elf M. Sternberg | f12b746ade | |
Elf M. Sternberg | 4895dde1b6 | |
Elf M. Sternberg | d046268ad6 | |
Elf M. Sternberg | 1b5b215e3b | |
Elf M. Sternberg | 556c56fb43 | |
Elf M. Sternberg | a1b5ae9eb0 |
|
@ -10,4 +10,4 @@ Development Lead
|
||||||
Contributors
|
Contributors
|
||||||
------------
|
------------
|
||||||
|
|
||||||
None yet. Why not be the first?
|
* Tino de Bruijn <work@tino.io>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
GitLint is a git command to automatically run a suite of pre-defined linters.
|
|
@ -8,5 +8,6 @@ recursive-include tests *
|
||||||
recursive-exclude * __pycache__
|
recursive-exclude * __pycache__
|
||||||
recursive-exclude * *.py[co]
|
recursive-exclude * *.py[co]
|
||||||
|
|
||||||
recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif
|
recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif *.1
|
||||||
include docs/_build/man/git-lint.1
|
include docs/_build/man/git-lint.1
|
||||||
|
include bin/pre-commit bin/git-lint-style
|
||||||
|
|
32
README.rst
32
README.rst
|
@ -34,9 +34,18 @@ To lint what's in your staging directory:
|
||||||
`git lint -s`
|
`git lint -s`
|
||||||
|
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Complete documentation for the project is available in the docs directory, or at `Git
|
||||||
|
Linter Docs <https://elfsternberg.github.io/git-linter/index.html>`_.
|
||||||
|
|
||||||
|
|
||||||
Install
|
Install
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
This *ought* to work:
|
||||||
|
|
||||||
`pip install git-linter`
|
`pip install git-linter`
|
||||||
|
|
||||||
You will need to copy the .git-lint configuration file to either your
|
You will need to copy the .git-lint configuration file to either your
|
||||||
|
@ -44,6 +53,13 @@ home directory or the repo`s base directory. Edit the configuration
|
||||||
file as needed. You will also need any linters that you plan on
|
file as needed. You will also need any linters that you plan on
|
||||||
running.
|
running.
|
||||||
|
|
||||||
|
As git-linter is still mostly alpha code, it might be better to install
|
||||||
|
from source:
|
||||||
|
|
||||||
|
``
|
||||||
|
git clone https://github.com/elfsternberg/git-linter
|
||||||
|
python setup.py install
|
||||||
|
``
|
||||||
|
|
||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
|
@ -66,12 +82,18 @@ Features
|
||||||
|
|
||||||
Acknowledgements
|
Acknowledgements
|
||||||
----------------
|
----------------
|
||||||
`Git lint` started life as a simple pre-commit hook. Most of the
|
|
||||||
changes since were inspired by Steve Pulec's *`Why You Need a Git
|
|
||||||
Pre-Commit Hook and Why Most Are Wrong`_ ,* as well as just my own needs as
|
|
||||||
a software developer.
|
|
||||||
|
|
||||||
.. _Why You Need a Git Pre-Commit Hook and Why Most Are Wrong: https://dzone.com/articles/why-your-need-git-pre-commit
|
`Git lint` started life as a simple pre-commit hook. Most of the changes since were
|
||||||
|
inspired by Steve Pulec's `Why You Need a Git Pre-Commit Hook and Why Most Are Wrong <https://dzone.com/articles/why-your-need-git-pre-commit>`_, as well as just my own needs
|
||||||
|
as a software developer.
|
||||||
|
|
||||||
|
To do
|
||||||
|
-----
|
||||||
|
|
||||||
|
* The '-q' and '--quiet' arguments do not work.
|
||||||
|
* Ought to be able to silence the "no linter found" message.
|
||||||
|
* Ought to be able to configure '-q' and '--silence' commands in .git-lint/config file.
|
||||||
|
* Ought to be able to override config file for above.
|
||||||
|
|
||||||
|
|
||||||
Disclaimer
|
Disclaimer
|
||||||
|
|
|
@ -111,7 +111,7 @@ pygments_style = 'sphinx'
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
html_theme = 'default'
|
html_theme = 'alabaster'
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a
|
# Theme options are theme-specific and customize the look and feel of a
|
||||||
# theme further. For a list of options available for each theme, see the
|
# theme further. For a list of options available for each theme, see the
|
||||||
|
|
|
@ -3,8 +3,12 @@
|
||||||
You can adapt this file completely to your liking, but it should at least
|
You can adapt this file completely to your liking, but it should at least
|
||||||
contain the root `toctree` directive.
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
Welcome to Git Lint's documentation!
|
Git Lint
|
||||||
======================================
|
========
|
||||||
|
|
||||||
|
Git Lint provides a new git command that automatically runs identifiable linters (style
|
||||||
|
and syntax quality assurance programs) against changed files in your current git
|
||||||
|
repository or staging area.
|
||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ To install Git Lint, run this command in your terminal:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
$ pip install git_linter
|
$ pip install git-linter
|
||||||
|
|
||||||
If you don't have `pip`_ installed, this `Python installation guide`_ can guide
|
If you don't have `pip`_ installed, this `Python installation guide`_ can guide
|
||||||
you through the process.
|
you through the process.
|
||||||
|
@ -30,13 +30,13 @@ You can either clone the public repository:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
$ git clone git://github.com/elfsternberg/git_linter
|
$ git clone git://github.com/elfsternberg/git-linter
|
||||||
|
|
||||||
Or download the `tarball`_:
|
Or download the `tarball`_:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
$ curl -OL https://github.com/elfsternberg/git_linter/tarball/master
|
$ curl -OL https://github.com/elfsternberg/git-linter/tarball/master
|
||||||
|
|
||||||
Once you have a copy of the source, you can install it with:
|
Once you have a copy of the source, you can install it with:
|
||||||
|
|
||||||
|
@ -44,9 +44,8 @@ Once you have a copy of the source, you can install it with:
|
||||||
|
|
||||||
$ python setup.py install
|
$ python setup.py install
|
||||||
|
|
||||||
.. _Github repo: https://github.com/elfsternberg/git_linter
|
.. _Github repo: https://github.com/elfsternberg/git-linter
|
||||||
.. _tarball: https://github.com/elfsternberg/git_linter/tarball/master
|
.. _tarball: https://github.com/elfsternberg/git-linter/tarball/master
|
||||||
|
|
||||||
Once installed, please copy the '.git-lint' example file. You may install this either in
|
Once installed, please copy the '.git-lint' example file. You may install this either in
|
||||||
your home directory as ``.git-lint`` or in your project's git directory as
|
your home or repository directory as ``.git-lint``.
|
||||||
``.git/lint/git-lint``
|
|
||||||
|
|
|
@ -12,25 +12,13 @@ git lint [options] [filenames]
|
||||||
Options
|
Options
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. include: arguments.rst
|
.. include:: arguments.rst
|
||||||
|
|
||||||
As a pre-commit hook:
|
As a pre-commit hook:
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
.. code-block:: python
|
There's a file, pre-commit, in the /bin directory with the project. (Or you can download
|
||||||
|
it from the github repository.) Install it in you .git/hooks/pre-commit file, and
|
||||||
|
chmod +x .git/hooks/pre-commit.
|
||||||
|
|
||||||
#!/usr/bin/env python
|
The pre-commit hook is *experimental*. Please be careful with it.
|
||||||
import git_lint
|
|
||||||
git_lint.run_precommit(staging = True, timestamps = True)
|
|
||||||
|
|
||||||
Install this file in your project's ``.git/hooks/pre-commit``, and set
|
|
||||||
the file's executable flag to ``true``:
|
|
||||||
|
|
||||||
.. code-block:: shell
|
|
||||||
|
|
||||||
chmod +x pre-commit
|
|
||||||
|
|
||||||
Please see the :ref:`api` for more details on options taken by the
|
|
||||||
``run_precommit()`` and ``run_gitlint`` commands.
|
|
||||||
|
|
||||||
There is an example ``pre-commit`` script shipped with ``git lint``.
|
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
__author__ = 'Kenneth M. "Elf" Sternberg'
|
__author__ = 'Kenneth M. "Elf" Sternberg'
|
||||||
__email__ = 'elf.sternberg@gmail.com'
|
__email__ = 'elf.sternberg@gmail.com'
|
||||||
__version__ = '0.0.4'
|
__version__ = '0.0.7'
|
||||||
|
|
||||||
__all__ = ['git_lint']
|
__all__ = ['git_lint']
|
||||||
|
|
|
@ -11,7 +11,7 @@ import gettext
|
||||||
_ = gettext.gettext
|
_ = gettext.gettext
|
||||||
|
|
||||||
NAME = 'git-lint'
|
NAME = 'git-lint'
|
||||||
VERSION = '0.0.4'
|
VERSION = '0.0.7'
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
@ -9,11 +9,18 @@ import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import configparser
|
import configparser
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
import ConfigParser as configparser
|
import ConfigParser as configparser
|
||||||
|
|
||||||
|
try: # noqa: F401
|
||||||
|
from typing import Dict, List, Text, Any, Optional, Union, Callable, Tuple # noqa: F401
|
||||||
|
except: # noqa: F401
|
||||||
|
pass # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
_ = gettext.gettext
|
_ = gettext.gettext
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,39 +31,6 @@ _ = gettext.gettext
|
||||||
# |___/
|
# |___/
|
||||||
|
|
||||||
|
|
||||||
def find_config_file(options, base):
|
|
||||||
""" Returns the configuration file from a prioritized list of locations.
|
|
||||||
|
|
||||||
Locations are prioritized as:
|
|
||||||
1. From the command line. Fail if specified but not found
|
|
||||||
2. The repository's root directory, as the file .git-lint
|
|
||||||
3. The repository's root directory, as the file .git-lint/config
|
|
||||||
4. The user's home directory, as file .git-lint
|
|
||||||
5. The user's home directory, as the file .git-lint/config
|
|
||||||
|
|
||||||
If no configuration file is found, this is an error.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if 'config' in options:
|
|
||||||
config = options['config']
|
|
||||||
configpath = os.path.abspath(config)
|
|
||||||
if not os.path.isfile(configpath):
|
|
||||||
sys.exit(_('Configuration file not found: {}\n').format(config))
|
|
||||||
return configpath
|
|
||||||
|
|
||||||
home = os.environ.get('HOME', None)
|
|
||||||
possibles = [os.path.join(base, '.git-lint'),
|
|
||||||
os.path.join(base, '.git-lint/config')] + ((home and [
|
|
||||||
os.path.join(home, '.git-lint'),
|
|
||||||
os.path.join(home, '.git-lint/config')]) or [])
|
|
||||||
|
|
||||||
matches = [p for p in possibles if os.path.isfile(p)]
|
|
||||||
if len(matches) == 0:
|
|
||||||
sys.exit(_('No configuration file found, tried: {}').format(':'.join(possibles)))
|
|
||||||
|
|
||||||
return matches[0]
|
|
||||||
|
|
||||||
|
|
||||||
# (commandLineDictionary, repositoryLocation) -> (configurationDictionary | exit)
|
# (commandLineDictionary, repositoryLocation) -> (configurationDictionary | exit)
|
||||||
def load_config(options, base):
|
def load_config(options, base):
|
||||||
"""Loads the git-lint configuration file.
|
"""Loads the git-lint configuration file.
|
||||||
|
@ -70,6 +44,38 @@ def load_config(options, base):
|
||||||
files for specific linters.
|
files for specific linters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def find_config_file(options, base):
|
||||||
|
""" Returns the configuration file from a prioritized list of locations.
|
||||||
|
|
||||||
|
Locations are prioritized as:
|
||||||
|
1. From the command line. Fail if specified but not found
|
||||||
|
2. The repository's root directory, as the file .git-lint
|
||||||
|
3. The repository's root directory, as the file .git-lint/config
|
||||||
|
4. The user's home directory, as file .git-lint
|
||||||
|
5. The user's home directory, as the file .git-lint/config
|
||||||
|
|
||||||
|
If no configuration file is found, this is an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if 'config' in options:
|
||||||
|
config = options['config']
|
||||||
|
configpath = os.path.abspath(config)
|
||||||
|
if not os.path.isfile(configpath):
|
||||||
|
sys.exit(_('Configuration file not found: {}\n').format(config))
|
||||||
|
return configpath
|
||||||
|
|
||||||
|
home = os.environ.get('HOME', None)
|
||||||
|
possibles = [os.path.join(base, '.git-lint'),
|
||||||
|
os.path.join(base, '.git-lint/config')] + ((home and [
|
||||||
|
os.path.join(home, '.git-lint'),
|
||||||
|
os.path.join(home, '.git-lint/config')]) or [])
|
||||||
|
|
||||||
|
matches = [p for p in possibles if os.path.isfile(p)]
|
||||||
|
if len(matches) == 0:
|
||||||
|
sys.exit(_('No configuration file found, tried: {}').format(':'.join(possibles)))
|
||||||
|
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
Linter = namedtuple('Linter', ['name', 'linter'])
|
Linter = namedtuple('Linter', ['name', 'linter'])
|
||||||
path = find_config_file(options, base)
|
path = find_config_file(options, base)
|
||||||
configloader = configparser.SafeConfigParser()
|
configloader = configparser.SafeConfigParser()
|
||||||
|
@ -168,37 +174,34 @@ class MatchFilter:
|
||||||
# \___|_||_\___\__|_\_\ |_|_|_||_\__\___|_| /__/
|
# \___|_||_\___\__|_\_\ |_|_|_||_\__\___|_| /__/
|
||||||
#
|
#
|
||||||
|
|
||||||
def executable_exists(script, label):
|
def linter_exists(linter, label):
|
||||||
if not len(script):
|
if not len(linter):
|
||||||
sys.exit(
|
sys.exit(_('Syntax error in linter configuration for {} ').format(label))
|
||||||
_('Syntax error in command configuration for {} ').format(label))
|
|
||||||
|
|
||||||
scriptname = script.split(' ').pop(0)
|
lintername = linter.split(' ').pop(0)
|
||||||
if not len(scriptname):
|
if not len(lintername):
|
||||||
sys.exit(
|
sys.exit(_('Syntax error in linter configuration for {} ').format(label))
|
||||||
_('Syntax error in command configuration for {} ').format(label))
|
|
||||||
|
|
||||||
def is_executable(path):
|
def is_executable(path):
|
||||||
return os.path.exists(path) and os.access(path, os.X_OK)
|
return os.path.exists(path) and os.access(path, os.X_OK)
|
||||||
|
|
||||||
if scriptname.startswith('/'):
|
if lintername.startswith('/'):
|
||||||
return (is_executable(scriptname) and scriptname) or None
|
return (is_executable(lintername) and lintername) or None
|
||||||
|
|
||||||
# shutil.which() doesn't appear until Python 3, darnit.
|
# shutil.which() doesn't appear until Python 3, darnit.
|
||||||
possibles = [path for path in
|
possibles = [path for path in
|
||||||
[os.path.join(path, scriptname)
|
[os.path.join(path, lintername)
|
||||||
for path in os.environ.get('PATH').split(':')]
|
for path in os.environ.get('PATH').split(':')]
|
||||||
if is_executable(path)]
|
if is_executable(path)]
|
||||||
|
|
||||||
return (len(possibles) and possibles.pop(0)) or False
|
return (len(possibles) and possibles.pop(0)) or False
|
||||||
|
|
||||||
|
|
||||||
def get_working_linter_names(config):
|
|
||||||
return [i.name for i in config
|
|
||||||
if executable_exists(i.linter['command'], i.name)]
|
|
||||||
|
|
||||||
|
|
||||||
def get_linter_status(config):
|
def get_linter_status(config):
|
||||||
|
def get_working_linter_names(config):
|
||||||
|
return [i.name for i in config
|
||||||
|
if linter_exists(i.linter['command'], i.name)]
|
||||||
|
|
||||||
working_linter_names = get_working_linter_names(config)
|
working_linter_names = get_working_linter_names(config)
|
||||||
broken_linter_names = (set([i.name for i in config]) - set(working_linter_names))
|
broken_linter_names = (set([i.name for i in config]) - set(working_linter_names))
|
||||||
return working_linter_names, broken_linter_names
|
return working_linter_names, broken_linter_names
|
||||||
|
@ -272,6 +275,11 @@ def get_filelist(options, extras):
|
||||||
|
|
||||||
return check_for_conflicts(parse_stream([], stream))
|
return check_for_conflicts(parse_stream([], stream))
|
||||||
|
|
||||||
|
def revision_list():
|
||||||
|
cmd = ['diff', '--name-only', '-z', options.get('revision')]
|
||||||
|
return [entry for entry in get_git_response(cmd).split(u'\x00')
|
||||||
|
if len(entry) > 0]
|
||||||
|
|
||||||
def staging_list():
|
def staging_list():
|
||||||
""" Return the list of files added or modified to the stage """
|
""" Return the list of files added or modified to the stage """
|
||||||
|
|
||||||
|
@ -303,6 +311,9 @@ def get_filelist(options, extras):
|
||||||
working_directory_trans = base_file_filter
|
working_directory_trans = base_file_filter
|
||||||
|
|
||||||
file_list_generator = working_list
|
file_list_generator = working_list
|
||||||
|
if 'revision' in options:
|
||||||
|
file_list_generator = revision_list
|
||||||
|
working_directory_trans = base_file_filter
|
||||||
if 'all' in options:
|
if 'all' in options:
|
||||||
file_list_generator = all_list
|
file_list_generator = all_list
|
||||||
if 'staging' in options:
|
if 'staging' in options:
|
||||||
|
@ -421,6 +432,9 @@ class Linters:
|
||||||
|
|
||||||
|
|
||||||
def run_linters(options, config, extras=[]):
|
def run_linters(options, config, extras=[]):
|
||||||
|
if 'pr' in options:
|
||||||
|
options.pop('pr')
|
||||||
|
options['revision'] = 'HEAD^..HEAD'
|
||||||
|
|
||||||
def build_config_subset(keys):
|
def build_config_subset(keys):
|
||||||
""" Returns a subset of the configuration, with only those linters mentioned in keys """
|
""" Returns a subset of the configuration, with only those linters mentioned in keys """
|
||||||
|
@ -430,17 +444,25 @@ def run_linters(options, config, extras=[]):
|
||||||
all_filenames, unfindable_filenames = get_filelist(options, extras)
|
all_filenames, unfindable_filenames = get_filelist(options, extras)
|
||||||
|
|
||||||
is_lintable = MatchFilter(config)
|
is_lintable = MatchFilter(config)
|
||||||
|
|
||||||
lintable_filenames = set([filename for filename in all_filenames
|
lintable_filenames = set([filename for filename in all_filenames
|
||||||
if is_lintable(filename)])
|
if is_lintable(filename)])
|
||||||
|
|
||||||
unlintable_filenames = set(all_filenames) - lintable_filenames
|
unlintable_filenames = set(all_filenames) - lintable_filenames
|
||||||
|
|
||||||
|
# Filter the linter config down to the selected ones.
|
||||||
|
if 'only' in options:
|
||||||
|
config = [linter for linter in config
|
||||||
|
if linter.name in options['only']]
|
||||||
|
elif 'exclude' in options:
|
||||||
|
config = [linter for linter in config
|
||||||
|
if linter.name not in options['exclude']]
|
||||||
|
if not len(config):
|
||||||
|
raise RuntimeError('No linters left to run! Be less strict with --only and --exclude.')
|
||||||
|
|
||||||
working_linter_names, broken_linter_names = get_linter_status(config)
|
working_linter_names, broken_linter_names = get_linter_status(config)
|
||||||
|
|
||||||
cant_lint_filter = MatchFilter(build_config_subset(
|
cant_lint_filter = MatchFilter(build_config_subset(
|
||||||
broken_linter_names))
|
broken_linter_names))
|
||||||
|
|
||||||
cant_lint_filenames = [filename for filename in lintable_filenames
|
cant_lint_filenames = [filename for filename in lintable_filenames
|
||||||
if cant_lint_filter(filename)]
|
if cant_lint_filter(filename)]
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,18 @@
|
||||||
# Author: Elf M. Sternberg
|
# Author: Elf M. Sternberg
|
||||||
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from collections import namedtuple
|
||||||
import getopt
|
import getopt
|
||||||
|
|
||||||
|
try: # noqa: F401
|
||||||
|
from typing import Dict, List, Text, Any, Optional, Union, Callable, Tuple # noqa: F401
|
||||||
|
except: # noqa: F401
|
||||||
|
pass # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
Option = namedtuple('Option', ['short', 'long', 'takes', 'help', 'conflicts']) # type: str, str, str, str, List[str]
|
||||||
|
Arguments = namedtuple('Arguments', ['arguments', 'filenames', 'excluded']) # type: Dict[str, str], List[str], List[str]
|
||||||
|
|
||||||
# This was a lot shorter and smarter in Hy...
|
# This was a lot shorter and smarter in Hy...
|
||||||
|
|
||||||
# A lot of what you see here is separated from git_lint itself, since this will not be
|
# A lot of what you see here is separated from git_lint itself, since this will not be
|
||||||
|
@ -17,6 +27,7 @@ import getopt
|
||||||
|
|
||||||
|
|
||||||
def cleanup_options(options, commandline):
|
def cleanup_options(options, commandline):
|
||||||
|
# type: (List[Option], List[str]) -> Arguments
|
||||||
"""Takes a table of options and the commandline, and returns a
|
"""Takes a table of options and the commandline, and returns a
|
||||||
dictionary of those options that appear on the commandline
|
dictionary of those options that appear on the commandline
|
||||||
along with any extra arguments.
|
along with any extra arguments.
|
||||||
|
@ -24,41 +35,50 @@ def cleanup_options(options, commandline):
|
||||||
:param List(Tuple (string, string, boolean, string, List(string))) options,
|
:param List(Tuple (string, string, boolean, string, List(string))) options,
|
||||||
The table of options: One-letter option, long option, takes arguments,
|
The table of options: One-letter option, long option, takes arguments,
|
||||||
Help text, list of (long) options superseded by this one.
|
Help text, list of (long) options superseded by this one.
|
||||||
|
|
||||||
: param List(strings) commandline
|
: param List(strings) commandline
|
||||||
The arguments as received by the start-up process
|
The arguments as received by the start-up process
|
||||||
|
|
||||||
|
: returns List(strings), List(strings), List(strings)
|
||||||
|
The longopt dictionary of arguments and associated values (if any)
|
||||||
|
The list of filenames left after argument processing
|
||||||
|
The longopt list of arguments that were excluded by argument precedence
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def make_option_streamliner(options):
|
def make_option_streamliner(options):
|
||||||
|
# type: (List[Option]) -> Callable[[Dict[str, str], Option], Dict[str, str]]
|
||||||
|
|
||||||
"""Takes a list of option tuples, and returns a function that takes
|
"""Takes a list of option tuples, and returns a function that takes
|
||||||
the output of getopt and reduces it to the longopt key and
|
the output of getopt and reduces it to the longopt key and
|
||||||
associated values as a dictionary.
|
associated values as a dictionary.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fullset = {}
|
fullset = {} # type: Dict[str, str]
|
||||||
for option in options:
|
for option in options:
|
||||||
if option[1]:
|
if option.long:
|
||||||
fullset['--' + option[1]] = option[1]
|
fullset['--' + option.long] = option.long
|
||||||
if option[0]:
|
if option.short:
|
||||||
fullset['-' + option[0]] = option[1]
|
fullset['-' + option.short] = option.long
|
||||||
|
|
||||||
def streamliner(acc, it):
|
def streamliner(acc, it):
|
||||||
|
# type: (Dict[str, str], Option) -> Dict[str, str]
|
||||||
acc[fullset[it[0]]] = it[1]
|
acc[fullset[it[0]]] = it[1]
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
return streamliner
|
return streamliner
|
||||||
|
|
||||||
def remove_conflicted_options(options, request):
|
def remove_conflicted_options(options, request):
|
||||||
|
# type: (List[Option], Dict[str, str]) -> Tuple[List[str], List[str]]
|
||||||
"""Takes our list of option tuples, and a cleaned copy of what was
|
"""Takes our list of option tuples, and a cleaned copy of what was
|
||||||
requested from getopt, and returns a copy of the request
|
requested from getopt, and returns a copy of the request
|
||||||
without any options that are marked as superseded, along with
|
without any options that are marked as superseded, along with
|
||||||
the list of superseded options
|
the list of superseded options
|
||||||
"""
|
"""
|
||||||
def get_excluded_keys(memo, opt):
|
def get_excluded_keys(memo, option):
|
||||||
return memo + ((len(opt) > 4 and opt[4]) or [])
|
return memo + option.conflicts
|
||||||
|
|
||||||
keys = request.keys()
|
keys = request.keys()
|
||||||
marked = [option for option in options if option[1] in keys]
|
marked = [option for option in options if option.long in keys]
|
||||||
exclude = reduce(get_excluded_keys, marked, [])
|
exclude = reduce(get_excluded_keys, marked, [])
|
||||||
excluded = [key for key in keys if key in exclude]
|
excluded = [key for key in keys if key in exclude]
|
||||||
cleaned = {key: request[key] for key in keys
|
cleaned = {key: request[key] for key in keys
|
||||||
|
@ -66,12 +86,12 @@ def cleanup_options(options, commandline):
|
||||||
return (cleaned, excluded)
|
return (cleaned, excluded)
|
||||||
|
|
||||||
def shortoptstogo(i):
|
def shortoptstogo(i):
|
||||||
return i[0] + ((i[2] and ':') or '')
|
return i.short + ((i.takes and ':') or '')
|
||||||
|
|
||||||
def longoptstogo(i):
|
def longoptstogo(i):
|
||||||
return i[1] + ((i[2] and '=') or '')
|
return i.long + ((i.takes and '=') or '')
|
||||||
|
|
||||||
optstringsshort = ''.join([shortoptstogo(opt) for opt in options])
|
optstringsshort = ''.join([shortoptstogo(opt) for opt in options if opt.short])
|
||||||
optstringslong = [longoptstogo(opt) for opt in options]
|
optstringslong = [longoptstogo(opt) for opt in options]
|
||||||
(chosen_options, filenames) = getopt.getopt(commandline[1:],
|
(chosen_options, filenames) = getopt.getopt(commandline[1:],
|
||||||
optstringsshort,
|
optstringsshort,
|
||||||
|
@ -84,4 +104,4 @@ def cleanup_options(options, commandline):
|
||||||
(ret, excluded) = remove_conflicted_options(
|
(ret, excluded) = remove_conflicted_options(
|
||||||
options, reduce(streamline_options, chosen_options, {}))
|
options, reduce(streamline_options, chosen_options, {}))
|
||||||
|
|
||||||
return (ret, filenames, excluded)
|
return Arguments(ret, filenames, excluded)
|
||||||
|
|
|
@ -1,39 +1,45 @@
|
||||||
import gettext
|
import gettext
|
||||||
|
from option_handler import Option
|
||||||
|
|
||||||
_ = gettext.gettext
|
_ = gettext.gettext
|
||||||
|
|
||||||
OPTIONS = [
|
OPTIONS = [
|
||||||
('o', 'only', True,
|
Option('o', 'only', True,
|
||||||
_('A comma-separated list of only those linters to run'), ['exclude']),
|
_('A comma-separated list of only those linters to run'), ['exclude']),
|
||||||
('x', 'exclude', True,
|
Option('x', 'exclude', True,
|
||||||
_('A comma-separated list of linters to skip'), []),
|
_('A comma-separated list of linters to skip'), []),
|
||||||
('l', 'linters', False,
|
Option('l', 'linters', False,
|
||||||
_('Show the list of configured linters'), []),
|
_('Show the list of configured linters'), []),
|
||||||
('b', 'base', False,
|
Option('b', 'base', False,
|
||||||
_('Check all changed files in the repository, not just those in the current directory.'), []),
|
_('Check all changed files in the repository, not just those in the current directory.'), []),
|
||||||
('a', 'all', False,
|
Option('a', 'all', False,
|
||||||
_('Scan all files in the repository, not just those that have changed.'), []),
|
_('Scan all files in the repository, not just those that have changed.'), ['revision']),
|
||||||
('e', 'every', False,
|
Option('r', 'revision', True,
|
||||||
_('Short for -b -a: scan everything'), []),
|
_('Scan all files changed between revisions'), []),
|
||||||
('w', 'workspace', False,
|
Option(None, 'pr', False,
|
||||||
_('Scan the workspace'), ['staging']),
|
_('Scan all files changed between head and previous check-in'), ['revision']),
|
||||||
('s', 'staging', False,
|
Option('e', 'every', False,
|
||||||
_('Scan the staging area (useful for pre-commit).'), []),
|
_('Short for -b -a: scan everything'), []),
|
||||||
|
Option('w', 'workspace', False,
|
||||||
|
_('Scan the workspace'), ['staging']),
|
||||||
|
Option('s', 'staging', False,
|
||||||
|
_('Scan the staging area (useful for pre-commit).'), []),
|
||||||
# ('g', 'changes', False,
|
# ('g', 'changes', False,
|
||||||
# _("Report lint failures only for diff'd sections"), ['complete']),
|
# _("Report lint failures only for diff'd sections"), ['complete']),
|
||||||
# ('p', 'complete', False,
|
# ('p', 'complete', False,
|
||||||
# _('Report lint failures for all files'), []),
|
# _('Report lint failures for all files'), []),
|
||||||
('t', 'bylinter', False,
|
Option('t', 'bylinter', False,
|
||||||
_('Group the reports by linter first as they appear in the config file [default]'), []),
|
_('Group the reports by linter first as they appear in the config file [default]'), []),
|
||||||
('f', 'byfile', False,
|
Option('f', 'byfile', False,
|
||||||
_('Group the reports by file first'), []),
|
_('Group the reports by file first'), []),
|
||||||
('d', 'dryrun', False,
|
Option('d', 'dryrun', False,
|
||||||
_('Dry run - report what would be done, but do not run linters'), []),
|
_('Dry run - report what would be done, but do not run linters'), []),
|
||||||
('c', 'config', True,
|
Option('c', 'config', True,
|
||||||
_('Path to config file'), []),
|
_('Path to config file'), []),
|
||||||
('h', 'help', False,
|
Option('h', 'help', False,
|
||||||
_('This help message'), []),
|
_('This help message'), []),
|
||||||
('V', 'verbose', False,
|
Option('V', 'verbose', False,
|
||||||
_('A slightly more verbose output'), []),
|
_('A slightly more verbose output'), []),
|
||||||
('v', 'version', False,
|
Option('v', 'version', False,
|
||||||
_('Version information'), [])
|
_('Version information'), [])
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
from functools import reduce
|
||||||
from .git_lint import load_config, run_linters, git_base
|
from .git_lint import load_config, run_linters, git_base
|
||||||
import operator
|
import operator
|
||||||
import gettext
|
import gettext
|
||||||
_ = gettext.gettext
|
_ = gettext.gettext
|
||||||
|
|
||||||
|
|
||||||
def base_file_cleaner(files):
|
|
||||||
return [file.replace(git_base + '/', '', 1) for file in files]
|
|
||||||
|
|
||||||
|
|
||||||
# ICK. Mutation, references, and hidden assignment.
|
|
||||||
def group_by(iterable, field_id):
|
|
||||||
results = []
|
|
||||||
keys = {}
|
|
||||||
for obj in iterable:
|
|
||||||
key = obj[field_id]
|
|
||||||
if key in keys:
|
|
||||||
keys[key].append(obj)
|
|
||||||
continue
|
|
||||||
keys[key] = [obj]
|
|
||||||
results.append((key, keys[key]))
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def print_report(results, unlintable_filenames, cant_lint_filenames,
|
def print_report(results, unlintable_filenames, cant_lint_filenames,
|
||||||
broken_linter_names, unfindable_filenames, options={'bylinter': True}):
|
broken_linter_names, unfindable_filenames, options={'bylinter': True}):
|
||||||
|
|
||||||
|
def base_file_cleaner(files):
|
||||||
|
return [file.replace(git_base + '/', '', 1) for file in files]
|
||||||
|
|
||||||
|
# ICK. Mutation, references, and hidden assignment.
|
||||||
|
def group_by(iterable, field_id):
|
||||||
|
results = []
|
||||||
|
keys = {}
|
||||||
|
for obj in iterable:
|
||||||
|
key = obj[field_id]
|
||||||
|
if key in keys:
|
||||||
|
keys[key].append(obj)
|
||||||
|
continue
|
||||||
|
keys[key] = [obj]
|
||||||
|
results.append((key, keys[key]))
|
||||||
|
return results
|
||||||
|
|
||||||
sort_position = 1
|
sort_position = 1
|
||||||
grouping = _('Linter: {}')
|
grouping = _('Linter: {}')
|
||||||
if 'byfile' in options:
|
if 'byfile' in options:
|
||||||
|
@ -44,9 +44,9 @@ def print_report(results, unlintable_filenames, cant_lint_filenames,
|
||||||
print ('')
|
print ('')
|
||||||
|
|
||||||
if len(broken_linter_names) and (len(cant_lint_filenames) or ('verbose' in options)):
|
if len(broken_linter_names) and (len(cant_lint_filenames) or ('verbose' in options)):
|
||||||
print(_('These linters could not be run:'), ','.join(broken_linter_names))
|
print(_('Linters not found:'), ','.join(broken_linter_names))
|
||||||
if len(cant_lint_filenames):
|
if len(cant_lint_filenames):
|
||||||
print(_('Files not linted:'))
|
print(' ' + _('Files not linted:'))
|
||||||
print('\n'.join([' {}'.format(f) for f in cant_lint_filenames]))
|
print('\n'.join([' {}'.format(f) for f in cant_lint_filenames]))
|
||||||
print('')
|
print('')
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ def print_report(results, unlintable_filenames, cant_lint_filenames,
|
||||||
print('')
|
print('')
|
||||||
|
|
||||||
if len(unfindable_filenames):
|
if len(unfindable_filenames):
|
||||||
print(_('Files not be found:'))
|
print(_('Files not found:'))
|
||||||
print('\n'.join([' {}'.format(f) for f in unfindable_filenames]))
|
print('\n'.join([' {}'.format(f) for f in unfindable_filenames]))
|
||||||
print('')
|
print('')
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ def print_report(results, unlintable_filenames, cant_lint_filenames,
|
||||||
def print_help(options, name):
|
def print_help(options, name):
|
||||||
print(_('Usage: {} [options] [filenames]').format(name))
|
print(_('Usage: {} [options] [filenames]').format(name))
|
||||||
for item in options:
|
for item in options:
|
||||||
print(' -{:<1} --{:<12} {}'.format(item[0], item[1], item[3]))
|
print(' {:<2} --{:<12} {}'.format((item[0] and ('-' + item[0])) or '', item[1], item[3]))
|
||||||
|
|
||||||
|
|
||||||
def print_version(name, version):
|
def print_version(name, version):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.0.4
|
current_version = 0.0.7
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
|
|
||||||
|
@ -16,3 +16,4 @@ universal = 1
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = docs
|
exclude = docs
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -39,7 +39,7 @@ test_requirements = [
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='git_linter',
|
name='git_linter',
|
||||||
version='0.0.4',
|
version='0.0.7',
|
||||||
description="A git command to lint everything in your workspace (or stage) that was changed since the last commit.",
|
description="A git command to lint everything in your workspace (or stage) that was changed since the last commit.",
|
||||||
long_description=readme + '\n\n' + history,
|
long_description=readme + '\n\n' + history,
|
||||||
author='Kenneth M. "Elf" Sternberg',
|
author='Kenneth M. "Elf" Sternberg',
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
#!/usr/bin/hy ; -*- mode: clojure -*-
|
|
||||||
(import subprocess os sys os.path)
|
|
||||||
|
|
||||||
(defn fmit [formatstring iterable]
|
|
||||||
(apply .format (+ [formatstring] (list iterable))))
|
|
||||||
|
|
||||||
(defmain [&rest args]
|
|
||||||
(let [[git_dir (subprocess.check_output ["git" "rev-parse" "--show-toplevel"])]]
|
|
||||||
(.append sys.path (os.path.join git_dir "git_lint"))
|
|
||||||
(import [git_lint [git_lint]])
|
|
||||||
(print (.join "\n" (map (fn [i] (if (get i 2)
|
|
||||||
(+ (fmit "\item[\oOptArg{{-{0}}}{{ names}} " i)
|
|
||||||
(fmit "\oOptArg{{--{1}}}={{ names}}] " i)
|
|
||||||
(fmit "{3}" i))
|
|
||||||
(+ (fmit "\item[\oOptArg{{-{0}}} " i)
|
|
||||||
(fmit "\oOptArg{{--{1}}}] " i)
|
|
||||||
(fmit "{3}" i))))
|
|
||||||
git_lint.OPTIONS_LIST)))))
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue