From 13b0378b679d4f82b65f55c7d4cedc48b82eae9a Mon Sep 17 00:00:00 2001 From: "Kenneth M. Elf Sternberg" Date: Mon, 3 Oct 2016 12:13:35 -0700 Subject: [PATCH] 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. --- README.rst | 57 ++++++++++++++++++++++++++++++++---------- git_lint/git_lint.py | 43 ++++++++++++++++--------------- git_lint/options.py | 2 ++ git_lint/reporters.py | 21 +++++++++++++--- tests/test_git_lint.py | 11 ++++++++ 5 files changed, 97 insertions(+), 37 deletions(-) diff --git a/README.rst b/README.rst index b0abc4e..4ec51ad 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,49 @@ =============================== -Git Lint +Git Lint: README =============================== 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 **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` command. -Some commands may require shell wrappers. +staging area. It can be configured to work with any `lint`-like +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` + + +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 -------- @@ -37,11 +63,16 @@ Features restoration of your workspace, it ensures the timestamps are the same, so as not to confuse your build system or IDE. -Credits -------- -The completion of this project was graciously sponsored by my employer, -Splunk . +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 + Disclaimer ---------- diff --git a/git_lint/git_lint.py b/git_lint/git_lint.py index 82a5e16..24d0e8f 100644 --- a/git_lint/git_lint.py +++ b/git_lint/git_lint.py @@ -318,33 +318,33 @@ def get_filelist(options, extras): # |___/ |___/ |_| |_| -class Runner: - def __init__(self, options): - self.runner = ('staging' in options and Runner.staging_wrapper) or Runner.workspace_wrapper +class StagingRunner: + def __init__(self, filenames): + self.filenames = filenames - def __call__(self, run_linters, filenames): - return self.runner(run_linters, filenames) - - @staticmethod - def staging_wrapper(run_linters, filenames): + def __enter__(self): def time_gather(f): stats = os.stat(f) return (f, (stats.st_atime, stats.st_mtime)) - - times = [time_gather(file) for file in filenames] + self.times = [time_gather(filename) for filename in self.filenames] run_git_command(['stash', '--keep-index']) - results = run_linters() + def __exit__(self, type, value, traceback): run_git_command(['reset', '--hard']) run_git_command(['stash', 'pop', '--quiet', '--index']) - - for (filename, timepair) in times: + for (filename, timepair) in self.times: os.utime(filename, timepair) - return results - @staticmethod - def workspace_wrapper(run_linters, filenames): - return run_linters() + +class WorkspaceRunner(object): + 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. 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 + '"' @@ -444,7 +444,9 @@ def run_linters(options, config, extras=[]): cant_lint_filenames = [filename for filename in lintable_filenames if cant_lint_filter(filename)] - runner = Runner(options) + runner = WorkspaceRunner + if 'staging' in options: + runner = StagingRunner linters = Linters(build_config_subset(working_linter_names), sorted(lintable_filenames)) @@ -454,7 +456,8 @@ def run_linters(options, config, extras=[]): return (dryrun_results, unlintable_filenames, cant_lint_filenames, broken_linter_names, unfindable_filenames) - results = runner(linters, lintable_filenames) + with runner(lintable_filenames): + results = linters() return (results, unlintable_filenames, cant_lint_filenames, broken_linter_names, unfindable_filenames) diff --git a/git_lint/options.py b/git_lint/options.py index 4de6235..3946942 100644 --- a/git_lint/options.py +++ b/git_lint/options.py @@ -32,6 +32,8 @@ OPTIONS = [ _('Path to config file'), []), ('h', 'help', False, _('This help message'), []), + ('V', 'verbose', False, + _('A slightly more verbose output'), []), ('v', 'version', False, _('Version information'), []) ] diff --git a/git_lint/reporters.py b/git_lint/reporters.py index 35e0a8e..9db3daa 100644 --- a/git_lint/reporters.py +++ b/git_lint/reporters.py @@ -1,5 +1,6 @@ from __future__ import print_function from .git_lint import load_config, run_linters, git_base +import operator import gettext _ = gettext.gettext @@ -30,22 +31,34 @@ def print_report(results, unlintable_filenames, cant_lint_filenames, 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]: - print('\n'.join(base_file_cleaner(text))) - print('') - if len(broken_linter_names): + 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(_('These linters could not be run:'), ','.join(broken_linter_names)) if len(cant_lint_filenames): print(_('As a result, these files were not linted:')) 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('\n'.join([' {}'.format(f) for f in unlintable_filenames])) + print('') + if len(unfindable_filenames): print(_('The following files could not be found:')) print('\n'.join([' {}'.format(f) for f in unfindable_filenames])) + print('') def print_help(options, name): diff --git a/tests/test_git_lint.py b/tests/test_git_lint.py index de820fc..e7d5206 100644 --- a/tests/test_git_lint.py +++ b/tests/test_git_lint.py @@ -102,6 +102,17 @@ 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)