Config is now order-preserving; switched to named tuples for config details.
This commit is contained in:
parent
ec439aab81
commit
24ba4d41f4
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from collections import namedtuple
|
||||||
import getopt
|
import getopt
|
||||||
import gettext
|
import gettext
|
||||||
import operator
|
import operator
|
||||||
|
@ -8,24 +9,15 @@ import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import pprint
|
import pprint
|
||||||
from git_lint_options import make_rational_options
|
try:
|
||||||
from git_lint_config import get_config
|
import configparser
|
||||||
|
except ImportError as e:
|
||||||
|
import ConfigParser as configparser
|
||||||
|
|
||||||
_ = gettext.gettext
|
_ = gettext.gettext
|
||||||
|
|
||||||
VERSION = '0.0.4'
|
VERSION = '0.0.4'
|
||||||
NAME = 'git-lint'
|
NAME = 'git-lint'
|
||||||
|
|
||||||
def tap(a):
|
|
||||||
print("TAP:", a)
|
|
||||||
return a
|
|
||||||
|
|
||||||
# ___ _ _ _
|
|
||||||
# / __|___ _ __ _ __ __ _ _ _ __| | | | (_)_ _ ___
|
|
||||||
# | (__/ _ \ ' \| ' \/ _` | ' \/ _` | | |__| | ' \/ -_)
|
|
||||||
# \___\___/_|_|_|_|_|_\__,_|_||_\__,_| |____|_|_||_\___|
|
|
||||||
#
|
|
||||||
|
|
||||||
OPTIONS_LIST = [
|
OPTIONS_LIST = [
|
||||||
('o', 'only', True,
|
('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']),
|
||||||
|
@ -54,7 +46,14 @@ OPTIONS_LIST = [
|
||||||
('h', 'help', False,
|
('h', 'help', False,
|
||||||
_('This help message'), []),
|
_('This help message'), []),
|
||||||
('v', 'version', False,
|
('v', 'version', False,
|
||||||
_('Version information'), [])]
|
_('Version information'), [])
|
||||||
|
]
|
||||||
|
|
||||||
|
# ___ _ _ _
|
||||||
|
# / __|___ _ __ _ __ __ _ _ _ __| | | | (_)_ _ ___
|
||||||
|
# | (__/ _ \ ' \| ' \/ _` | ' \/ _` | | |__| | ' \/ -_)
|
||||||
|
# \___\___/_|_|_|_|_|_\__,_|_||_\__,_| |____|_|_||_\___|
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
# This was a lot shorter and smarter in Hy...
|
# This was a lot shorter and smarter in Hy...
|
||||||
|
@ -96,7 +95,7 @@ def make_rational_options(optlist, args):
|
||||||
the list of superseded options
|
the list of superseded options
|
||||||
"""
|
"""
|
||||||
def get_excluded_keys(memo, opt):
|
def get_excluded_keys(memo, opt):
|
||||||
return memo + (len(opt) > 4 and opt[4] or [])
|
return memo + ((len(opt) > 4 and opt[4]) or [])
|
||||||
|
|
||||||
keys = request.keys()
|
keys = request.keys()
|
||||||
marked = [option for option in optlist if option[1] in keys]
|
marked = [option for option in optlist if option[1] in keys]
|
||||||
|
@ -107,10 +106,10 @@ def make_rational_options(optlist, args):
|
||||||
return (cleaned, excluded)
|
return (cleaned, excluded)
|
||||||
|
|
||||||
def shortoptstogo(i):
|
def shortoptstogo(i):
|
||||||
return i[0] + (i[2] and ':' or '')
|
return i[0] + ((i[2] and ':') or '')
|
||||||
|
|
||||||
def longoptstogo(i):
|
def longoptstogo(i):
|
||||||
return i[1] + (i[2] and '=' or '')
|
return i[1] + ((i[2] and '=') or '')
|
||||||
|
|
||||||
optstringsshort = ''.join([shortoptstogo(opt) for opt in optlist])
|
optstringsshort = ''.join([shortoptstogo(opt) for opt in optlist])
|
||||||
optstringslong = [longoptstogo(opt) for opt in optlist]
|
optstringslong = [longoptstogo(opt) for opt in optlist]
|
||||||
|
@ -126,6 +125,74 @@ def make_rational_options(optlist, args):
|
||||||
|
|
||||||
return (retoptions, filenames, excluded)
|
return (retoptions, filenames, excluded)
|
||||||
|
|
||||||
|
# ___ __ _ ___ _
|
||||||
|
# / __|___ _ _ / _(_)__ _ | _ \___ __ _ __| |___ _ _
|
||||||
|
# | (__/ _ \ ' \| _| / _` | | / -_) _` / _` / -_) '_|
|
||||||
|
# \___\___/_||_|_| |_\__, | |_|_\___\__,_\__,_\___|_|
|
||||||
|
# |___/
|
||||||
|
|
||||||
|
|
||||||
|
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.path.join(os.environ.get('HOME'))
|
||||||
|
possibles = (os.path.join(base, '.git-lint'),
|
||||||
|
os.path.join(base, '.git-lint/config'),
|
||||||
|
os.path.join(home, '.git-lint'),
|
||||||
|
os.path.join(home, '.git-lint/config'))
|
||||||
|
|
||||||
|
matches = [p for p in possibles if os.path.isfile(p)]
|
||||||
|
if len(matches) == 0:
|
||||||
|
sys.exit(_('No configuration file found'))
|
||||||
|
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
|
||||||
|
# (commandLineDictionary, repositoryLocation) -> (configurationDictionary | exit)
|
||||||
|
def get_config(options, base):
|
||||||
|
"""Loads the git-lint configuration file.
|
||||||
|
|
||||||
|
Returns the configuration file as a dictionary of dictionaries.
|
||||||
|
Performs substitutions as specified in the SafeConfigParser
|
||||||
|
specification; the only one performed currently is the 'repodir'
|
||||||
|
will be replaced with the base directory of the repository.
|
||||||
|
Combined with the option to specify the .git-lint configuration as
|
||||||
|
a directory, this allows users to keep per-project configuration
|
||||||
|
files for specific linters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Linter = namedtuple('Linter', ['name', 'linter'])
|
||||||
|
path = find_config_file(options, base)
|
||||||
|
configloader = configparser.SafeConfigParser()
|
||||||
|
configloader.read(path)
|
||||||
|
configloader.set('DEFAULT', 'repodir', base)
|
||||||
|
return [Linter(section, {k: v for (k, v) in configloader.items(section)})
|
||||||
|
for section in configloader.sections()]
|
||||||
|
|
||||||
|
def ckeys(config):
|
||||||
|
return [i.name for i in config]
|
||||||
|
|
||||||
|
def cvals(config):
|
||||||
|
return [i.linter for i in config]
|
||||||
|
|
||||||
|
|
||||||
# ___ _ _
|
# ___ _ _
|
||||||
# / __(_) |_
|
# / __(_) |_
|
||||||
# | (_ | | _|
|
# | (_ | | _|
|
||||||
|
@ -173,14 +240,14 @@ def get_shell_response(fullcmd):
|
||||||
def get_git_base():
|
def get_git_base():
|
||||||
(out, error, returncode) = get_git_response_raw(
|
(out, error, returncode) = get_git_response_raw(
|
||||||
['rev-parse', '--show-toplevel'])
|
['rev-parse', '--show-toplevel'])
|
||||||
return returncode == 0 and out.rstrip() or None
|
return (returncode == 0 and out.rstrip()) or None
|
||||||
|
|
||||||
|
|
||||||
def get_git_head():
|
def get_git_head():
|
||||||
empty_repository_hash = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
|
empty_repository_hash = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
|
||||||
(out, err, returncode) = get_git_response_raw(
|
(out, err, returncode) = get_git_response_raw(
|
||||||
['rev-parse', '--verify HEAD'])
|
['rev-parse', '--verify HEAD'])
|
||||||
return (err and empty_repository_hash or 'HEAD')
|
return ((err and empty_repository_hash) or 'HEAD')
|
||||||
|
|
||||||
|
|
||||||
git_base = get_git_base()
|
git_base = get_git_base()
|
||||||
|
@ -194,7 +261,7 @@ git_head = get_git_head()
|
||||||
#
|
#
|
||||||
|
|
||||||
def base_file_cleaner(files):
|
def base_file_cleaner(files):
|
||||||
return [file.replace(git_base, '', 1) for file in files]
|
return [file.replace(git_base + '/', '', 1) for file in files]
|
||||||
|
|
||||||
|
|
||||||
def make_match_filter_matcher(extensions):
|
def make_match_filter_matcher(extensions):
|
||||||
|
@ -205,8 +272,7 @@ def make_match_filter_matcher(extensions):
|
||||||
|
|
||||||
|
|
||||||
def make_match_filter(config):
|
def make_match_filter(config):
|
||||||
matcher = make_match_filter_matcher([v.get('match', '')
|
matcher = make_match_filter_matcher([v.linter.get('match', '') for v in config])
|
||||||
for v in config.values()])
|
|
||||||
|
|
||||||
def match_filter(path):
|
def match_filter(path):
|
||||||
return matcher.search(path)
|
return matcher.search(path)
|
||||||
|
@ -234,31 +300,29 @@ def executable_exists(script, label):
|
||||||
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 scriptname.startswith('/'):
|
||||||
return is_executable(scriptname) and scriptname or None
|
return (is_executable(scriptname) and scriptname) or None
|
||||||
|
|
||||||
possibles = [path for path in
|
possibles = [path for path in
|
||||||
[os.path.join(path, scriptname)
|
[os.path.join(path, scriptname)
|
||||||
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 None
|
return (len(possibles) and possibles.pop(0)) or None
|
||||||
|
|
||||||
|
|
||||||
def get_working_linters(config):
|
def get_working_linter_names(config):
|
||||||
return set([key for key in config.keys()
|
return [i.name for i in config
|
||||||
if executable_exists(config[key]['command'], key)])
|
if executable_exists(i.linter['command'], i.name)]
|
||||||
|
|
||||||
|
|
||||||
def print_linters(config):
|
def print_linters(config):
|
||||||
print(_('Currently supported linters:'))
|
print(_('Currently supported linters:'))
|
||||||
working = get_working_linters(config)
|
working = get_working_linter_names(config)
|
||||||
broken = (set(config.keys()) - working)
|
broken = set([i.name for i in config]) - set(working)
|
||||||
for key in sorted(working):
|
for linter in config:
|
||||||
print('{:<14} {}'.format(key, config[key].get('comment', '')))
|
print('{:<14} {}'.format(linter.name,
|
||||||
for key in sorted(broken):
|
((linter.name in broken and
|
||||||
print('{:<14} {}'.format(key, _('(WARNING: executable not found)')))
|
_('(WARNING: executable not found)') or
|
||||||
|
linter.linter.get('comment', '')))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ___ _ _ _ _ __ __ _ _
|
# ___ _ _ _ _ __ __ _ _
|
||||||
|
@ -354,11 +418,9 @@ def get_filelist(cmdline, extras):
|
||||||
|
|
||||||
if len(extras):
|
if len(extras):
|
||||||
cwd = os.path.abspath(os.getcwd())
|
cwd = os.path.abspath(os.getcwd())
|
||||||
extras_fullpathed = set([os.path.join(cwd, f) for f in extras])
|
extras_fullpathed = set([os.path.abspath(os.path.join(cwd, f)) for f in extras])
|
||||||
not_found = set([f for f in extras_fullpathed if not os.path.isfile(f)])
|
not_found = set([f for f in extras_fullpathed if not os.path.isfile(f)])
|
||||||
for f in not_found:
|
return ([os.path.relpath(f, cwd) for f in (extras_fullpathed - not_found)], not_found)
|
||||||
print(_("File not found: {}").format(f))
|
|
||||||
return [os.path.relpath(f, cwd) for f in (extras_fullpathed - not_found)]
|
|
||||||
|
|
||||||
working_directory_trans = cwd_file_filter
|
working_directory_trans = cwd_file_filter
|
||||||
if 'base' in cmdline or 'every' in cmdline:
|
if 'base' in cmdline or 'every' in cmdline:
|
||||||
|
@ -370,7 +432,7 @@ def get_filelist(cmdline, extras):
|
||||||
if 'staging' in keys:
|
if 'staging' in keys:
|
||||||
file_list_generator = staging_list
|
file_list_generator = staging_list
|
||||||
|
|
||||||
return working_directory_trans(remove_submodules(file_list_generator()))
|
return (working_directory_trans(remove_submodules(file_list_generator())), [])
|
||||||
|
|
||||||
|
|
||||||
# ___ _ _
|
# ___ _ _
|
||||||
|
@ -409,7 +471,7 @@ def pick_stash_runner(cmdline):
|
||||||
def workspace_wrapper(run_linters):
|
def workspace_wrapper(run_linters):
|
||||||
return run_linters()
|
return run_linters()
|
||||||
|
|
||||||
return 'staging' in cmdline and staging_wrapper or workspace_wrapper
|
return ('staging' in cmdline and staging_wrapper) or workspace_wrapper
|
||||||
|
|
||||||
# ___ _ _ _
|
# ___ _ _ _
|
||||||
# | _ \_ _ _ _ | (_)_ _| |_ _ __ __ _ ______
|
# | _ \_ _ _ _ | (_)_ _| |_ _ __ __ _ ______
|
||||||
|
@ -437,8 +499,8 @@ def run_external_linter(filename, linter, linter_name):
|
||||||
if not failed:
|
if not failed:
|
||||||
return (filename, linter_name, 0, [])
|
return (filename, linter_name, 0, [])
|
||||||
|
|
||||||
prefix = linter.get('print', False) and '\t{}:'.format(filename) or '\t'
|
prefix = (linter.get('print', False) and '\t{}: '.format(filename)) or '\t'
|
||||||
output = encode_shell_messages(prefix, out) + (err and encode_shell_messages(prefix, err) or [])
|
output = encode_shell_messages(prefix, out) + ((err and encode_shell_messages(prefix, err)) or [])
|
||||||
return (filename, linter_name, (returncode or 1), output)
|
return (filename, linter_name, (returncode or 1), output)
|
||||||
|
|
||||||
|
|
||||||
|
@ -483,36 +545,38 @@ def run_gitlint(cmdline, config, extras):
|
||||||
|
|
||||||
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 """
|
||||||
return {item[0]: item[1] for item in config.items() if item[0] in keys}
|
return [item for item in config if item.name in keys]
|
||||||
|
|
||||||
""" Runs the requested linters """
|
""" Runs the requested linters """
|
||||||
all_files = get_filelist(cmdline, extras)
|
(all_filenames, unfindable_filenames) = get_filelist(cmdline, extras)
|
||||||
stash_runner = pick_runner(cmdline)
|
|
||||||
is_lintable = make_match_filter(config)
|
stash_runner = pick_stash_runner(cmdline)
|
||||||
lintable_files = set([file for file in all_files if lintable(file)])
|
is_lintable = make_match_filter(config)
|
||||||
unlintable_files = (set(all_files) - lintable_files)
|
lintable_filenames = set([filename for filename in all_filenames if is_lintable(filename)])
|
||||||
|
unlintable_filenames = set(all_filenames) - lintable_filenames
|
||||||
|
|
||||||
|
working_linter_names = get_working_linter_names(config)
|
||||||
|
broken_linter_names = (set([i.name for i in config]) - set(working_linter_names))
|
||||||
|
cant_lint_filter = make_match_filter(build_config_subset(broken_linter_names))
|
||||||
|
cant_lint_filenames = [filename for filename in lintable_filenames if cant_lint_filter(filename)]
|
||||||
|
|
||||||
working_linters = get_working_linters(config)
|
|
||||||
broken_linters = (set(config) - set(working_linters))
|
|
||||||
cant_lint_filter = make_match_filter(build_config_subset(broken_linters))
|
|
||||||
cant_lint_files = set([file for file in lintable_files if cant_lint_filter(file)])
|
|
||||||
lint_runner = build_lint_runner(build_config_subset(working_linters), lintable_files)
|
lint_runner = build_lint_runner(build_config_subset(working_linters), lintable_files)
|
||||||
|
|
||||||
results = stash_runner(lint_runner)
|
results = stash_runner(lint_runner)
|
||||||
|
|
||||||
print(list(results))
|
print(list(results))
|
||||||
return max(*[i[2] for i in results if len(i)])
|
return max([i[2] for i in results if len(i)])
|
||||||
|
|
||||||
|
|
||||||
def print_help():
|
def print_help(options_list, name):
|
||||||
print(_('Usage: {} [options] [filenames]').format(NAME))
|
print(_('Usage: {} [options] [filenames]').format(name))
|
||||||
for item in OPTIONS_LIST:
|
for item in options_list:
|
||||||
print(' -{:<1} --{:<12} {}'.format(item[0], item[1], item[3]))
|
print(' -{:<1} --{:<12} {}'.format(item[0], item[1], item[3]))
|
||||||
return sys.exit()
|
return sys.exit()
|
||||||
|
|
||||||
|
|
||||||
def print_version():
|
def print_version(name, version):
|
||||||
print('{} {} Copyright (c) 2009, 2016 Kennth M. "Elf" Sternberg'.format(NAME, VERSION))
|
print('{} {} Copyright (c) 2009, 2016 Kennth M. "Elf" Sternberg'.format(name, version))
|
||||||
|
|
||||||
|
|
||||||
def main(*args):
|
def main(*args):
|
||||||
|
@ -524,17 +588,17 @@ def main(*args):
|
||||||
if len(excluded_commands) > 0:
|
if len(excluded_commands) > 0:
|
||||||
print(_('These command line options were ignored due to option precedence.'))
|
print(_('These command line options were ignored due to option precedence.'))
|
||||||
for exc in excluded_commands:
|
for exc in excluded_commands:
|
||||||
print "\t{}".format(exc)
|
print("\t{}".format(exc))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = get_config(cmdline, git_base)
|
config = get_config(cmdline, git_base)
|
||||||
|
|
||||||
if 'help' in cmdline:
|
if 'help' in cmdline:
|
||||||
print_help()
|
print_help(OPTIONS_LIST, NAME)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if 'version' in cmdline:
|
if 'version' in cmdline:
|
||||||
print_version()
|
print_version(NAME, VERSION)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if 'linters' in cmdline:
|
if 'linters' in cmdline:
|
||||||
|
|
Loading…
Reference in New Issue