Compare commits

..

1 Commits

Author SHA1 Message Date
Elf M. Sternberg 739075d2df Experimental and not yet reliable. 2016-09-30 11:15:10 -07:00
24 changed files with 328 additions and 442 deletions

View File

@ -63,9 +63,3 @@ match = .json
print = False
condition = error
[readability]
output = Running Text Style Check
match = .rst
command = git-lint-style --min=6 --max=12
print = False
condition = error

View File

@ -10,4 +10,4 @@ Development Lead
Contributors
------------
* Tino de Bruijn <work@tino.io>
None yet. Why not be the first?

View File

@ -1 +0,0 @@
GitLint is a git command to automatically run a suite of pre-defined linters.

View File

@ -8,6 +8,4 @@ recursive-include tests *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif *.1
include docs/_build/man/git-lint.1
include bin/pre-commit bin/git-lint-style
recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif

View File

@ -12,7 +12,6 @@ export BROWSER_PYSCRIPT
BROWSER := python -c "$$BROWSER_PYSCRIPT"
help:
@echo "all - local build including docs"
@echo "clean - remove all build, test, coverage and Python artifacts"
@echo "clean-build - remove build artifacts"
@echo "clean-pyc - remove Python file artifacts"
@ -22,14 +21,10 @@ help:
@echo "test-all - run tests on every Python version with tox"
@echo "coverage - check code coverage quickly with the default Python"
@echo "docs - generate Sphinx HTML documentation, including API docs"
@echo "docsbrowse - generate Sphinx HTML documentation and start browser"
@echo "release - package and upload a release"
@echo "dist - package"
@echo "install - install the package to the active Python's site-packages"
all: docs
python setup.py build
clean: clean-build clean-pyc clean-test
clean-build:
@ -72,9 +67,6 @@ docs:
sphinx-apidoc -o docs/ git_lint
$(MAKE) -C docs clean
$(MAKE) -C docs html
$(MAKE) -C docs man
docbrowse: docs
$(BROWSER) docs/_build/html/index.html
servedocs: docs
@ -90,4 +82,4 @@ dist: clean
ls -l dist
install: clean
python setup.py install --prefix=/usr/local
python setup.py install

View File

@ -1,66 +1,24 @@
===============================
Git Lint: README
Git Lint
===============================
A git command that automatically runs identifiable linters against
changed files in your current git repository or staging area.
changed files in current repository or staging.
* Free software: MIT license
**Git Lint** runs a configurable set of syntax, style, and complexity
checkers against changed files in your current working directory or
staging area. It can be configured to work with any `lint`-like
command. Some commands may require shell wrappers.
staging area. It can be configured to work with any `lint` command.
Some commands may require shell wrappers.
While it may be possible to create a custom lint command in your npm,
grunt, Make, CMake, or whatever, the fact is we all use a VCS, and most
of us use git. Having a centralized repository for what we want checked
and how we want it checked, associated with git (and by extension, as a
pre-commit hook), that can be run at any time for any reason.
grunt, Make, CMake, or whatever, the fact is we all use a VCS, and
most of us use git. Having a centralized repository for what we want
checked and how we want it checked, associated with git (and by
extension, as a pre-commit hook), that can be run at any time for any
reason.
Usage
-----
To lint only what's changed recently in your current working directory:
`git lint`
To lint everything, changed or otherwise, from the current directory down:
`git lint -a`
To lint what's changed from the repo's base:
`git lint -b`
To lint what's in your staging directory:
`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
-------
This *ought* to work:
`pip install git-linter`
You will need to copy the .git-lint configuration file to either your
home directory or the repo`s base directory. Edit the configuration
file as needed. You will also need any linters that you plan on
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
--------
@ -79,22 +37,11 @@ Features
restoration of your workspace, it ensures the timestamps are the
same, so as not to confuse your build system or IDE.
Credits
-------
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 <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.
The completion of this project was graciously sponsored by my employer,
Splunk <http://splunk.com>.
Disclaimer
----------

4
bin/git-lint-style → bin/git-lint-style.py Executable file → Normal file
View File

@ -46,10 +46,6 @@ def main(*args):
print(error)
return returncode
kincaid = re.search(r'Kincaid:\s*([\d\.]+)', values, re.MULTILINE)
# Assumes an uncheckable issue, like an empty RST. Not unusual.
if not kincaid:
return 0
val = float(kincaid.group(1))
msg = ""

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python
from git_lint.git_lint import load_config, run_linters, git_base
from git_lint.reporters import print_report
from git_lint import load_config, run_linters, git_base
import gettext
_ = gettext.gettext
@ -21,14 +20,14 @@ def main(*args):
unlintable_filenames,
cant_lint_filenames,
broken_linter_names,
unfindable_filenames) = run_linters(pre_commit_options, config)
unfindable_filenames) = run_linters(options, config)
print_report(results,
unlintable_filenames,
cant_lint_filenames,
broken_linter_names,
unfindable_filenames,
pre_commit_options)
options)
if not len(results):
return 0

View File

@ -14,19 +14,12 @@
Scan the workspace [default]
**-s, --staging**
Scan the staging area (useful for pre-commit).
**-c <path>, --config=<path>**
Path to config file
**-t, --bylinter**
Group reports by linter first as they appear in the config file [default]
**-f, --byfile**
Group reports by file first, linter second
**-c** <path>, --config**=<path> Path to config file
**-d, --dryrun**
Report what git-lint would do, but don't actually do anything.
**-q, --quiet**
Produce a short report of file that failed to pass.
Produce a short report of files that failed to pass.
**-h, --help**
Print a short help message
**-V, --verbose**
Print a slightly more verbose long report
**-v, --version**
Print version information

View File

@ -20,7 +20,7 @@ import os
# directory, add these directories to sys.path here. If the directory is
# relative to the documentation root, use os.path.abspath to make it
# absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
#sys.path.insert(0, os.path.abspath('.'))
# Get the project root dir, which is the parent dir of this
cwd = os.getcwd()
@ -31,12 +31,12 @@ project_root = os.path.dirname(cwd)
# version is used.
sys.path.insert(0, project_root)
import git_lint # noqa
import git_lint
# -- General configuration ---------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
@ -49,7 +49,7 @@ templates_path = ['_templates']
source_suffix = '.rst'
# The encoding of source files.
# source_encoding = 'utf-8-sig'
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
@ -69,13 +69,13 @@ release = git_lint.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
#language = None
# There are two options for replacing |today|: either, you set today to
# some non-false value, then it is used:
# today = ''
#today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@ -83,60 +83,60 @@ exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built
# documents.
# keep_warnings = False
#keep_warnings = False
# -- Options for HTML output -------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'alabaster'
html_theme = 'default'
# 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
# documentation.
# html_theme_options = {}
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
# html_title = None
#html_title = None
# A shorter title for the navigation bar. Default is the same as
# html_title.
# html_short_title = None
#html_short_title = None
# The name of an image file (relative to this directory) to place at the
# top of the sidebar.
# html_logo = None
#html_logo = None
# The name of an image file (within the static path) to use as favicon
# of the docs. This file should be a Windows icon file (.ico) being
# 16x16 or 32x32 pixels large.
# html_favicon = None
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets)
# here, relative to this directory. They are copied after the builtin
@ -146,46 +146,46 @@ html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names
# to template names.
# html_additional_pages = {}
#html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
#html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
#html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
#html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer.
# Default is True.
# html_show_sphinx = True
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer.
# Default is True.
# html_show_copyright = True
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages
# will contain a <link> tag referring to it. The value of this option
# must be the base URL from which the finished HTML is served.
# html_use_opensearch = ''
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'git_lintdoc'
@ -195,13 +195,13 @@ htmlhelp_basename = 'git_lintdoc'
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
@ -215,23 +215,23 @@ latex_documents = [
# The name of an image file (relative to this directory) to place at
# the top of the title page.
# latex_logo = None
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings
# are parts, not chapters.
# latex_use_parts = False
#latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
#latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
#latex_domain_indices = True
# -- Options for manual page output ------------------------------------
@ -245,7 +245,7 @@ man_pages = [
]
# If true, show URL addresses after external links.
# man_show_urls = False
#man_show_urls = False
# -- Options for Texinfo output ----------------------------------------
@ -263,13 +263,13 @@ texinfo_documents = [
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
#texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
# texinfo_no_detailmenu = False
#texinfo_no_detailmenu = False

View File

@ -12,30 +12,6 @@ git_lint.git_lint module
:undoc-members:
:show-inheritance:
git_lint.option_handler module
------------------------------
.. automodule:: git_lint.option_handler
:members:
:undoc-members:
:show-inheritance:
git_lint.options module
-----------------------
.. automodule:: git_lint.options
:members:
:undoc-members:
:show-inheritance:
git_lint.reporters module
-------------------------
.. automodule:: git_lint.reporters
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -3,12 +3,8 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
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.
Welcome to Git Lint's documentation!
======================================
Contents:

View File

@ -12,7 +12,7 @@ To install Git Lint, run this command in your terminal:
.. code-block:: console
$ pip install git-linter
$ pip install git_lint
If you don't have `pip`_ installed, this `Python installation guide`_ can guide
you through the process.
@ -30,13 +30,13 @@ You can either clone the public repository:
.. code-block:: console
$ git clone git://github.com/elfsternberg/git-linter
$ git clone git://github.com/elfsternberg/git_lint
Or download the `tarball`_:
.. code-block:: console
$ curl -OL https://github.com/elfsternberg/git-linter/tarball/master
$ curl -OL https://github.com/elfsternberg/git_lint/tarball/master
Once you have a copy of the source, you can install it with:
@ -44,8 +44,10 @@ Once you have a copy of the source, you can install it with:
$ python setup.py install
.. _Github repo: https://github.com/elfsternberg/git-linter
.. _tarball: https://github.com/elfsternberg/git-linter/tarball/master
.. _Github repo: https://github.com/elfsternberg/git_lint
.. _tarball: https://github.com/elfsternberg/git_lint/tarball/master
Once installed, please copy the '.git-lint' example file. You may install this either in
your home or repository directory as ``.git-lint``.
Once installed, you may run the 'git lint --make-config' command, which
will generate a simple configuration file. You may install this either
in your home directory as ``.git-lint.conf`` or in your project's git
directory as ``.git/lint/git-lint.conf``

View File

@ -1,7 +1,7 @@
=====
Usage
=====
Command Line
------------
@ -12,13 +12,24 @@ git lint [options] [filenames]
Options
-------
.. include:: arguments.rst
.. include: arguments.rst
As a pre-commit hook:
---------------------
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.
.. code-block:: python
The pre-commit hook is *experimental*. Please be careful with it.
#!/usr/bin/env python
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.

View File

@ -2,6 +2,6 @@
__author__ = 'Kenneth M. "Elf" Sternberg'
__email__ = 'elf.sternberg@gmail.com'
__version__ = '0.0.7'
__version__ = '0.0.4'
__all__ = ['git_lint']

View File

@ -5,20 +5,75 @@ from .option_handler import cleanup_options
from .reporters import print_report, print_help, print_linters
from .git_lint import load_config, run_linters, git_base
from getopt import GetoptError
import os.path
import sys
import time
watchdog = False
try:
import watchdog
from watchdog.observers import Observer
from watchdog.events import RegexMatchingEventHandler
except Exception as e:
pass
import gettext
_ = gettext.gettext
NAME = 'git-lint'
VERSION = '0.0.7'
VERSION = '0.0.4'
def remove_unavailable_options(options):
failures = [] + ((watchdog == False and ['monitor']) or [])
return filter(lambda i: i[1] not in failures, options)
def monitor(options, config, filenames):
observer = watchdog.observers.Observer()
skip = ['\.git/']
def run_monitor_linters():
(results,
unlintable_filenames,
cant_lint_filenames,
broken_linter_names,
unfindable_filenames) = run_linters(options, config, filenames)
print_report(results,
unlintable_filenames,
cant_lint_filenames,
broken_linter_names,
unfindable_filenames,
options)
class LintMonitor(RegexMatchingEventHandler):
def __init__(self):
super(LintMonitor, self).__init__(ignore_regexes=skip)
def on_created(self, event):
run_monitor_linters()
def on_modified(self, event):
run_monitor_linters()
observer.schedule(LintMonitor(), git_base, recursive=True)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
def main():
if git_base is None:
sys.exit(_('A git repository was not found.'))
(options, filenames, excluded_commands) = cleanup_options(OPTIONS, sys.argv)
initial_options = remove_unavailable_options(OPTIONS)
(options, filenames, excluded_commands) = cleanup_options(initial_options, sys.argv)
if len(excluded_commands) > 0:
print(_('These command line options were ignored due to option precedence.'))
@ -29,7 +84,7 @@ def main():
config = load_config(options, git_base)
if 'help' in options:
print_help(OPTIONS, NAME)
print_help(initial_options, NAME)
return 0
if 'version' in options:
@ -43,6 +98,9 @@ def main():
print_linters(config, broken_linter_names)
return 0
if 'monitor' in options:
return monitor(options, config, filenames)
(results,
unlintable_filenames,
cant_lint_filenames,
@ -62,7 +120,7 @@ def main():
return max([i[2] for i in results if len(i)])
except GetoptError as err:
print_help(OPTIONS)
print_help(initial_options)
return 1

View File

@ -9,18 +9,11 @@ import re
import subprocess
import sys
import pprint
try:
import configparser
except ImportError as e:
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
@ -31,6 +24,39 @@ _ = 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)
def load_config(options, base):
"""Loads the git-lint configuration file.
@ -44,38 +70,6 @@ def load_config(options, base):
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'])
path = find_config_file(options, base)
configloader = configparser.SafeConfigParser()
@ -152,6 +146,10 @@ git_head = get_git_head()
# \___/ \__|_|_|_|\__|_\___/__/
#
def base_file_cleaner(files):
return [file.replace(git_base + '/', '', 1) for file in files]
class MatchFilter:
def __init__(self, config):
@ -174,34 +172,37 @@ class MatchFilter:
# \___|_||_\___\__|_\_\ |_|_|_||_\__\___|_| /__/
#
def linter_exists(linter, label):
if not len(linter):
sys.exit(_('Syntax error in linter configuration for {} ').format(label))
def executable_exists(script, label):
if not len(script):
sys.exit(
_('Syntax error in command configuration for {} ').format(label))
lintername = linter.split(' ').pop(0)
if not len(lintername):
sys.exit(_('Syntax error in linter configuration for {} ').format(label))
scriptname = script.split(' ').pop(0)
if not len(scriptname):
sys.exit(
_('Syntax error in command configuration for {} ').format(label))
def is_executable(path):
return os.path.exists(path) and os.access(path, os.X_OK)
if lintername.startswith('/'):
return (is_executable(lintername) and lintername) or None
if scriptname.startswith('/'):
return (is_executable(scriptname) and scriptname) or None
# shutil.which() doesn't appear until Python 3, darnit.
possibles = [path for path in
[os.path.join(path, lintername)
[os.path.join(path, scriptname)
for path in os.environ.get('PATH').split(':')]
if is_executable(path)]
return (len(possibles) and possibles.pop(0)) or False
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)]
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):
working_linter_names = get_working_linter_names(config)
broken_linter_names = (set([i.name for i in config]) - set(working_linter_names))
return working_linter_names, broken_linter_names
@ -275,11 +276,6 @@ def get_filelist(options, extras):
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():
""" Return the list of files added or modified to the stage """
@ -311,9 +307,6 @@ def get_filelist(options, extras):
working_directory_trans = base_file_filter
file_list_generator = working_list
if 'revision' in options:
file_list_generator = revision_list
working_directory_trans = base_file_filter
if 'all' in options:
file_list_generator = all_list
if 'staging' in options:
@ -329,33 +322,33 @@ def get_filelist(options, extras):
# |___/ |___/ |_| |_|
class StagingRunner:
def __init__(self, filenames):
self.filenames = filenames
class Runner:
def __init__(self, options):
self.runner = ('staging' in options and Runner.staging_wrapper) or Runner.workspace_wrapper
def __enter__(self):
def __call__(self, run_linters, filenames):
return self.runner(run_linters, filenames)
@staticmethod
def staging_wrapper(run_linters, filenames):
def time_gather(f):
stats = os.stat(f)
return (f, (stats.st_atime, stats.st_mtime))
self.times = [time_gather(filename) for filename in self.filenames]
times = [time_gather(file) for file in filenames]
run_git_command(['stash', '--keep-index'])
def __exit__(self, type, value, traceback):
results = run_linters()
run_git_command(['reset', '--hard'])
run_git_command(['stash', 'pop', '--quiet', '--index'])
for (filename, timepair) in self.times:
for (filename, timepair) in times:
os.utime(filename, timepair)
return results
class WorkspaceRunner(object):
def __init__(self, filenames):
pass
def __enter__(self):
pass
def __exit__(self, type, value, traceback):
pass
@staticmethod
def workspace_wrapper(run_linters, filenames):
return run_linters()
# ___ _ _ _
@ -379,7 +372,7 @@ class Linters:
"""Run one linter against one file.
If the result matches the error condition specified in the configuration file,
return the error code and messages, otherwise return nothing.
return the error code and messages, either return nothing.
"""
cmd = linter['command'] + ' "' + filename + '"'
@ -432,9 +425,6 @@ class Linters:
def run_linters(options, config, extras=[]):
if 'pr' in options:
options.pop('pr')
options['revision'] = 'HEAD^..HEAD'
def build_config_subset(keys):
""" Returns a subset of the configuration, with only those linters mentioned in keys """
@ -444,31 +434,21 @@ def run_linters(options, config, extras=[]):
all_filenames, unfindable_filenames = get_filelist(options, extras)
is_lintable = MatchFilter(config)
lintable_filenames = set([filename for filename in all_filenames
if is_lintable(filename)])
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)
cant_lint_filter = MatchFilter(build_config_subset(
broken_linter_names))
cant_lint_filenames = [filename for filename in lintable_filenames
if cant_lint_filter(filename)]
runner = WorkspaceRunner
if 'staging' in options:
runner = StagingRunner
runner = Runner(options)
linters = Linters(build_config_subset(working_linter_names),
sorted(lintable_filenames))
@ -478,8 +458,7 @@ def run_linters(options, config, extras=[]):
return (dryrun_results, unlintable_filenames, cant_lint_filenames,
broken_linter_names, unfindable_filenames)
with runner(lintable_filenames):
results = linters()
results = runner(linters, lintable_filenames)
return (results, unlintable_filenames, cant_lint_filenames,
broken_linter_names, unfindable_filenames)

View File

@ -2,18 +2,8 @@
# Author: Elf M. Sternberg
from functools import reduce
from collections import namedtuple
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...
# A lot of what you see here is separated from git_lint itself, since this will not be
@ -27,7 +17,6 @@ Arguments = namedtuple('Arguments', ['arguments', 'filenames', 'excluded']) # t
def cleanup_options(options, commandline):
# type: (List[Option], List[str]) -> Arguments
"""Takes a table of options and the commandline, and returns a
dictionary of those options that appear on the commandline
along with any extra arguments.
@ -35,67 +24,56 @@ def cleanup_options(options, commandline):
:param List(Tuple (string, string, boolean, string, List(string))) options,
The table of options: One-letter option, long option, takes arguments,
Help text, list of (long) options superseded by this one.
: param List(strings) commandline
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):
# type: (List[Option]) -> Callable[[Dict[str, str], Option], Dict[str, str]]
"""Takes a list of option tuples, and returns a function that takes
the output of getopt and reduces it to the longopt key and
associated values as a dictionary.
"""
fullset = {} # type: Dict[str, str]
fullset = {}
for option in options:
if option.long:
fullset['--' + option.long] = option.long
if option.short:
fullset['-' + option.short] = option.long
if option[1]:
fullset['--' + option[1]] = option[1]
if option[0]:
fullset['-' + option[0]] = option[1]
def streamliner(acc, it):
# type: (Dict[str, str], Option) -> Dict[str, str]
acc[fullset[it[0]]] = it[1]
return acc
return streamliner
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
requested from getopt, and returns a copy of the request
without any options that are marked as superseded, along with
the list of superseded options
"""
def get_excluded_keys(memo, option):
return memo + option.conflicts
def get_excluded_keys(memo, opt):
return memo + ((len(opt) > 4 and opt[4]) or [])
keys = request.keys()
marked = [option for option in options if option.long in keys]
marked = [option for option in options if option[1] in keys]
exclude = reduce(get_excluded_keys, marked, [])
excluded = [key for key in keys if key in exclude]
cleaned = {key: request[key] for key in keys
if key not in excluded}
return (cleaned, excluded)
def shortoptstogo(i):
return i.short + ((i.takes and ':') or '')
def shortoptstogo(i): return i[0] + ((i[2] and ':') or '')
def longoptstogo(i):
return i.long + ((i.takes and '=') or '')
def longoptstogo(i): return i[1] + ((i[2] and '=') or '')
optstringsshort = ''.join([shortoptstogo(opt) for opt in options if opt.short])
optstringsshort = ''.join([shortoptstogo(opt) for opt in options])
optstringslong = [longoptstogo(opt) for opt in options]
(chosen_options, filenames) = getopt.getopt(commandline[1:],
optstringsshort,
optstringslong)
optstringsshort,
optstringslong)
# Turns what getopt returns into something more human-readable
streamline_options = make_option_streamliner(options)
@ -104,4 +82,4 @@ def cleanup_options(options, commandline):
(ret, excluded) = remove_conflicted_options(
options, reduce(streamline_options, chosen_options, {}))
return Arguments(ret, filenames, excluded)
return (ret, filenames, excluded)

View File

@ -1,45 +1,40 @@
import gettext
from option_handler import Option
_ = gettext.gettext
OPTIONS = [
Option('o', 'only', True,
_('A comma-separated list of only those linters to run'), ['exclude']),
Option('x', 'exclude', True,
_('A comma-separated list of linters to skip'), []),
Option('l', 'linters', False,
_('Show the list of configured linters'), []),
Option('b', 'base', False,
_('Check all changed files in the repository, not just those in the current directory.'), []),
Option('a', 'all', False,
_('Scan all files in the repository, not just those that have changed.'), ['revision']),
Option('r', 'revision', True,
_('Scan all files changed between revisions'), []),
Option(None, 'pr', False,
_('Scan all files changed between head and previous check-in'), ['revision']),
Option('e', 'every', False,
_('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,
# _("Report lint failures only for diff'd sections"), ['complete']),
# ('p', 'complete', False,
# _('Report lint failures for all files'), []),
Option('t', 'bylinter', False,
_('Group the reports by linter first as they appear in the config file [default]'), []),
Option('f', 'byfile', False,
_('Group the reports by file first'), []),
Option('d', 'dryrun', False,
_('Dry run - report what would be done, but do not run linters'), []),
Option('c', 'config', True,
_('Path to config file'), []),
Option('h', 'help', False,
_('This help message'), []),
Option('V', 'verbose', False,
_('A slightly more verbose output'), []),
Option('v', 'version', False,
_('Version information'), [])
('o', 'only', True,
_('A comma-separated list of only those linters to run'), ['exclude']),
('x', 'exclude', True,
_('A comma-separated list of linters to skip'), []),
('l', 'linters', False,
_('Show the list of configured linters'), []),
('b', 'base', False,
_('Check all changed files in the repository, not just those in the current directory.'), []),
('a', 'all', False,
_('Scan all files in the repository, not just those that have changed.'), []),
('e', 'every', False,
_('Short for -b -a: scan everything'), []),
('w', 'workspace', False,
_('Scan the workspace'), ['staging']),
('s', 'staging', False,
_('Scan the staging area (useful for pre-commit).'), ['monitor']),
('g', 'changes', False,
_("Report lint failures only for diff'd sections"), ['complete']),
('p', 'complete', False,
_('Report lint failures for all files'), []),
('t', 'bylinter', False,
_('Group the reports by linter first as they appear in the config file [default]'), []),
('f', 'byfile', False,
_('Group the reports by file first'), []),
('d', 'dryrun', False,
_('Dry run - report what would be done, but do not run linters'), []),
('m', 'monitor', False,
_('Run continuously, monitoring filesytem for changes'), []),
('c', 'config', True,
_('Path to config file'), []),
('h', 'help', False,
_('This help message'), []),
('v', 'version', False,
_('Version information'), [])
]

View File

@ -1,70 +1,52 @@
from __future__ import print_function
from functools import reduce
from .git_lint import load_config, run_linters, git_base
import operator
import gettext
_ = gettext.gettext
# 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,
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
grouping = _('Linter: {}')
if 'byfile' in options:
sort_position = 0
grouping = _('Filename: {}')
grouped_results = group_by(results, sort_position)
for group in grouped_results:
messages = reduce(operator.add, [item[3] for item in group[1]], [])
if len(messages) == 0:
continue
print(grouping.format(group[0]))
for (filename, lintername, returncode, text) in group[1]:
if text:
print('\n'.join(base_file_cleaner(text)))
print('')
print ('')
if len(broken_linter_names) and (len(cant_lint_filenames) or ('verbose' in options)):
print(_('Linters not found:'), ','.join(broken_linter_names))
print('\n'.join(text))
print('')
if len(broken_linter_names):
print(_('These linters could not be run:'), ','.join(broken_linter_names))
if len(cant_lint_filenames):
print(' ' + _('Files not linted:'))
print(_('As a result, these files were not linted:'))
print('\n'.join([' {}'.format(f) for f in cant_lint_filenames]))
print('')
if len(unlintable_filenames) and ('verbose' in options):
print(_('No recognizeable linters for:'))
if len(unlintable_filenames):
print(_('The following files had no recognizeable linters:'))
print('\n'.join([' {}'.format(f) for f in unlintable_filenames]))
print('')
if len(unfindable_filenames):
print(_('Files not found:'))
print(_('The following files could not be found:'))
print('\n'.join([' {}'.format(f) for f in unfindable_filenames]))
print('')
def print_help(options, name):
print(_('Usage: {} [options] [filenames]').format(name))
for item in options:
print(' {:<2} --{:<12} {}'.format((item[0] and ('-' + item[0])) or '', item[1], item[3]))
print(' -{:<1} --{:<12} {}'.format(item[0], item[1], item[3]))
def print_version(name, version):

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.0.7
current_version = 0.0.4
commit = True
tag = True
@ -16,4 +16,3 @@ universal = 1
[flake8]
exclude = docs

View File

@ -1,22 +1,7 @@
#!/usr/bin/env python
import re
import sys
import argparse
import os.path
# -*- coding: utf-8 -*-
def get_data_files(prefix):
if prefix.startswith('/System/Library/Frameworks'):
return []
return [(os.path.join(prefix, 'share/man/man1'), ['docs/_build/man/git-lint.1'])]
parser = argparse.ArgumentParser()
parser.add_argument('--prefix', default='',
help='prefix to install data files')
opts, _ = parser.parse_known_args(sys.argv)
prefix = opts.prefix or sys.prefix or '/usr'
try:
from setuptools import setup
except ImportError:
@ -39,7 +24,7 @@ test_requirements = [
setup(
name='git_linter',
version='0.0.7',
version='0.0.4',
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,
author='Kenneth M. "Elf" Sternberg',
@ -51,11 +36,9 @@ setup(
package_dir={'git_lint':
'git_lint'},
include_package_data=True,
data_files=get_data_files(prefix),
install_requires=requirements,
license="MIT",
zip_safe=False,
scripts=['bin/git-lint-style'],
keywords='git lint style syntaxt development',
entry_points={
'console_scripts': [

View File

@ -102,17 +102,6 @@ def test_02_empty_repository():
assert stderr.startswith('No configuration file found,')
# It should behave well when the repository is empty and we're
# running against staging.
def test_02b_empty_repository():
with gittemp() as path:
os.chdir(path)
shell('git init')
(stdout, stderr, rc) = fullshell('git lint -s')
assert stderr.startswith('No configuration file found,')
def test_03_simple_repository():
with gittemp() as path:
os.chdir(path)

20
utils/optstotex.hy Normal file
View File

@ -0,0 +1,20 @@
#!/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)))))