Fixed reportage matrix.

Now it doesn't print things the user doesn't care about, like empty
lint passes, or broken linters that aren't relevant to the current
project.

Added a test pass to ensure it doesn't blow up when running against
an empty stage.
This commit is contained in:
Elf M. Sternberg 2016-10-03 12:13:35 -07:00
parent aa4b5aa978
commit 13b0378b67
5 changed files with 97 additions and 37 deletions

View File

@ -1,23 +1,49 @@
=============================== ===============================
Git Lint Git Lint: README
=============================== ===============================
A git command that automatically runs identifiable linters against A git command that automatically runs identifiable linters against
changed files in current repository or staging. changed files in your current git repository or staging area.
* Free software: MIT license * Free software: MIT license
**Git Lint** runs a configurable set of syntax, style, and complexity **Git Lint** runs a configurable set of syntax, style, and complexity
checkers against changed files in your current working directory or checkers against changed files in your current working directory or
staging area. It can be configured to work with any `lint` command. staging area. It can be configured to work with any `lint`-like
Some commands may require shell wrappers. command. Some commands may require shell wrappers.
While it may be possible to create a custom lint command in your npm, 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 grunt, Make, CMake, or whatever, the fact is we all use a VCS, and most
most of us use git. Having a centralized repository for what we want of us use git. Having a centralized repository for what we want checked
checked and how we want it checked, associated with git (and by and how we want it checked, associated with git (and by extension, as a
extension, as a pre-commit hook), that can be run at any time for any pre-commit hook), that can be run at any time for any reason.
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`
Install
-------
`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.
Features Features
-------- --------
@ -37,11 +63,16 @@ Features
restoration of your workspace, it ensures the timestamps are the restoration of your workspace, it ensures the timestamps are the
same, so as not to confuse your build system or IDE. same, so as not to confuse your build system or IDE.
Credits
-------
The completion of this project was graciously sponsored by my employer, Acknowledgements
Splunk <http://splunk.com>. ----------------
`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
Disclaimer Disclaimer
---------- ----------

View File

@ -318,33 +318,33 @@ def get_filelist(options, extras):
# |___/ |___/ |_| |_| # |___/ |___/ |_| |_|
class Runner: class StagingRunner:
def __init__(self, options): def __init__(self, filenames):
self.runner = ('staging' in options and Runner.staging_wrapper) or Runner.workspace_wrapper self.filenames = filenames
def __call__(self, run_linters, filenames): def __enter__(self):
return self.runner(run_linters, filenames)
@staticmethod
def staging_wrapper(run_linters, filenames):
def time_gather(f): def time_gather(f):
stats = os.stat(f) stats = os.stat(f)
return (f, (stats.st_atime, stats.st_mtime)) 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']) run_git_command(['stash', '--keep-index'])
results = run_linters() def __exit__(self, type, value, traceback):
run_git_command(['reset', '--hard']) run_git_command(['reset', '--hard'])
run_git_command(['stash', 'pop', '--quiet', '--index']) run_git_command(['stash', 'pop', '--quiet', '--index'])
for (filename, timepair) in self.times:
for (filename, timepair) in times:
os.utime(filename, timepair) os.utime(filename, timepair)
return results
@staticmethod
def workspace_wrapper(run_linters, filenames): class WorkspaceRunner(object):
return run_linters() def __init__(self, filenames):
pass
def __enter__(self):
pass
def __exit__(self, type, value, traceback):
pass
# ___ _ _ _ # ___ _ _ _
@ -368,7 +368,7 @@ class Linters:
"""Run one linter against one file. """Run one linter against one file.
If the result matches the error condition specified in the configuration file, If the result matches the error condition specified in the configuration file,
return the error code and messages, either return nothing. return the error code and messages, otherwise return nothing.
""" """
cmd = linter['command'] + ' "' + filename + '"' cmd = linter['command'] + ' "' + filename + '"'
@ -444,7 +444,9 @@ def run_linters(options, config, extras=[]):
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)]
runner = Runner(options) runner = WorkspaceRunner
if 'staging' in options:
runner = StagingRunner
linters = Linters(build_config_subset(working_linter_names), linters = Linters(build_config_subset(working_linter_names),
sorted(lintable_filenames)) sorted(lintable_filenames))
@ -454,7 +456,8 @@ def run_linters(options, config, extras=[]):
return (dryrun_results, unlintable_filenames, cant_lint_filenames, return (dryrun_results, unlintable_filenames, cant_lint_filenames,
broken_linter_names, unfindable_filenames) broken_linter_names, unfindable_filenames)
results = runner(linters, lintable_filenames) with runner(lintable_filenames):
results = linters()
return (results, unlintable_filenames, cant_lint_filenames, return (results, unlintable_filenames, cant_lint_filenames,
broken_linter_names, unfindable_filenames) broken_linter_names, unfindable_filenames)

View File

@ -32,6 +32,8 @@ OPTIONS = [
_('Path to config file'), []), _('Path to config file'), []),
('h', 'help', False, ('h', 'help', False,
_('This help message'), []), _('This help message'), []),
('V', 'verbose', False,
_('A slightly more verbose output'), []),
('v', 'version', False, ('v', 'version', False,
_('Version information'), []) _('Version information'), [])
] ]

View File

@ -1,5 +1,6 @@
from __future__ import print_function from __future__ import print_function
from .git_lint import load_config, run_linters, git_base from .git_lint import load_config, run_linters, git_base
import operator
import gettext import gettext
_ = gettext.gettext _ = gettext.gettext
@ -30,22 +31,34 @@ def print_report(results, unlintable_filenames, cant_lint_filenames,
sort_position = 0 sort_position = 0
grouping = _('Filename: {}') grouping = _('Filename: {}')
grouped_results = group_by(results, sort_position) grouped_results = group_by(results, sort_position)
for group in grouped_results: 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])) print(grouping.format(group[0]))
for (filename, lintername, returncode, text) in group[1]: for (filename, lintername, returncode, text) in group[1]:
print('\n'.join(base_file_cleaner(text))) if text:
print('') print('\n'.join(base_file_cleaner(text)))
if len(broken_linter_names): print('')
print ('')
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(_('These linters could not be run:'), ','.join(broken_linter_names))
if len(cant_lint_filenames): if len(cant_lint_filenames):
print(_('As a result, these files were not linted:')) print(_('As a result, these files were not linted:'))
print('\n'.join([' {}'.format(f) for f in cant_lint_filenames])) print('\n'.join([' {}'.format(f) for f in cant_lint_filenames]))
if len(unlintable_filenames): print('')
if len(unlintable_filenames) and ('verbose' in options):
print(_('The following files had no recognizeable linters:')) print(_('The following files had no recognizeable linters:'))
print('\n'.join([' {}'.format(f) for f in unlintable_filenames])) print('\n'.join([' {}'.format(f) for f in unlintable_filenames]))
print('')
if len(unfindable_filenames): if len(unfindable_filenames):
print(_('The following files could not be found:')) print(_('The following files could not be found:'))
print('\n'.join([' {}'.format(f) for f in unfindable_filenames])) print('\n'.join([' {}'.format(f) for f in unfindable_filenames]))
print('')
def print_help(options, name): def print_help(options, name):

View File

@ -102,6 +102,17 @@ def test_02_empty_repository():
assert stderr.startswith('No configuration file found,') 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(): def test_03_simple_repository():
with gittemp() as path: with gittemp() as path:
os.chdir(path) os.chdir(path)