Rough draft complete. I think.

This commit is contained in:
Elf M. Sternberg 2016-09-26 14:58:43 -07:00
parent 24ba4d41f4
commit d153fba7d0
1 changed files with 97 additions and 77 deletions

View File

@ -39,6 +39,10 @@ OPTIONS_LIST = [
_("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,
_('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, ('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, ('c', 'config', True,
@ -49,11 +53,11 @@ OPTIONS_LIST = [
_('Version information'), []) _('Version information'), [])
] ]
# ___ _ _ _ # ___ _ _ _
# / __|___ _ __ _ __ __ _ _ _ __| | | | (_)_ _ ___ # / __|___ _ __ _ __ __ _ _ _ __| | | | (_)_ _ ___
# | (__/ _ \ ' \| ' \/ _` | ' \/ _` | | |__| | ' \/ -_) # | (__/ _ \ ' \| ' \/ _` | ' \/ _` | | |__| | ' \/ -_)
# \___\___/_|_|_|_|_|_\__,_|_||_\__,_| |____|_|_||_\___| # \___\___/_|_|_|_|_|_\__,_|_||_\__,_| |____|_|_||_\___|
# #
# This was a lot shorter and smarter in Hy... # This was a lot shorter and smarter in Hy...
@ -65,28 +69,27 @@ def make_rational_options(optlist, args):
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.
""" """
def make_opt_assoc(prefix, pos): def make_opt_assoc(prefix, pos):
def associater(acc, it): def associater(acc, it):
acc[(prefix + it[pos])] = it[1] acc[(prefix + it[pos])] = it[1]
return acc return acc
return associater return associater
short_opt_assoc = make_opt_assoc('-', 0) short_opt_assoc = make_opt_assoc('-', 0)
long_opt_assoc = make_opt_assoc('--', 1) long_opt_assoc = make_opt_assoc('--', 1)
def make_full_set(acc, i): def make_full_set(acc, i):
return long_opt_assoc(short_opt_assoc(acc, i), i) return long_opt_assoc(short_opt_assoc(acc, i), i)
fullset = reduce(make_full_set, optlist, {}) fullset = reduce(make_full_set, optlist, {})
def rationalizer(acc, it): def rationalizer(acc, it):
acc[fullset[it[0]]] = it[1] acc[fullset[it[0]]] = it[1]
return acc return acc
return rationalizer return rationalizer
# (OptionTupleList, dictionaryOfOptions) -> (dictionaryOfOptions, excludedOptions) # (OptionTupleList, dictionaryOfOptions) -> (dictionaryOfOptions, excludedOptions)
def remove_conflicted_options(optlist, request): def remove_conflicted_options(optlist, request):
"""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
@ -96,7 +99,7 @@ def make_rational_options(optlist, args):
""" """
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]
exclude = reduce(get_excluded_keys, marked, []) exclude = reduce(get_excluded_keys, marked, [])
@ -104,7 +107,7 @@ def make_rational_options(optlist, args):
cleaned = {key: request[key] for key in keys cleaned = {key: request[key] for key in keys
if key not in excluded} if key not in excluded}
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 '')
@ -125,11 +128,11 @@ def make_rational_options(optlist, args):
return (retoptions, filenames, excluded) return (retoptions, filenames, excluded)
# ___ __ _ ___ _ # ___ __ _ ___ _
# / __|___ _ _ / _(_)__ _ | _ \___ __ _ __| |___ _ _ # / __|___ _ _ / _(_)__ _ | _ \___ __ _ __| |___ _ _
# | (__/ _ \ ' \| _| / _` | | / -_) _` / _` / -_) '_| # | (__/ _ \ ' \| _| / _` | | / -_) _` / _` / -_) '_|
# \___\___/_||_|_| |_\__, | |_|_\___\__,_\__,_\___|_| # \___\___/_||_|_| |_\__, | |_|_\___\__,_\__,_\___|_|
# |___/ # |___/
def find_config_file(options, base): def find_config_file(options, base):
@ -141,7 +144,7 @@ def find_config_file(options, base):
3. The repository's root directory, as the file .git-lint/config 3. The repository's root directory, as the file .git-lint/config
4. The user's home directory, as file .git-lint 4. The user's home directory, as file .git-lint
5. The user's home directory, as the file .git-lint/config 5. The user's home directory, as the file .git-lint/config
If no configuration file is found, this is an error. If no configuration file is found, this is an error.
""" """
@ -186,18 +189,12 @@ def get_config(options, base):
return [Linter(section, {k: v for (k, v) in configloader.items(section)}) return [Linter(section, {k: v for (k, v) in configloader.items(section)})
for section in configloader.sections()] 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] # / __(_) |_
# ___ _ _
# / __(_) |_
# | (_ | | _| # | (_ | | _|
# \___|_|\__| # \___|_|\__|
# #
def get_git_response_raw(cmd): def get_git_response_raw(cmd):
fullcmd = (['git'] + cmd) fullcmd = (['git'] + cmd)
@ -224,7 +221,7 @@ def run_git_command(cmd):
return subprocess.call(fullcmd, return subprocess.call(fullcmd,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True) universal_newlines=True)
def get_shell_response(fullcmd): def get_shell_response(fullcmd):
@ -254,18 +251,18 @@ git_base = get_git_base()
git_head = get_git_head() 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):
trimmed = [s.strip() for s in reduce(operator.add, trimmed = [s.strip() for s in reduce(operator.add,
[ex.split(',') for ex in extensions], [])] [ex.split(',') for ex in extensions], [])]
cleaned = [re.sub(r'^\.', '', s) for s in trimmed] cleaned = [re.sub(r'^\.', '', s) for s in trimmed]
return re.compile(r'\.' + '|'.join(cleaned) + r'$') return re.compile(r'\.' + '|'.join(cleaned) + r'$')
@ -280,11 +277,25 @@ def make_match_filter(config):
return match_filter return match_filter
# ___ _ _ _ _ _ # 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 executable_exists(script, label): def executable_exists(script, label):
if not len(script): if not len(script):
@ -306,7 +317,7 @@ def executable_exists(script, label):
[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 False
def get_working_linter_names(config): def get_working_linter_names(config):
@ -314,39 +325,41 @@ def get_working_linter_names(config):
if executable_exists(i.linter['command'], i.name)] 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
def print_linters(config): def print_linters(config):
print(_('Currently supported linters:')) print(_('Currently supported linters:'))
working = get_working_linter_names(config) working_linter_names, broken_linter_names = get_linter_status(config)
broken = set([i.name for i in config]) - set(working)
for linter in config: for linter in config:
print('{:<14} {}'.format(linter.name, print('{:<14} {}'.format(linter.name,
((linter.name in broken and ((linter.name in broken_linter_names and
_('(WARNING: executable not found)') or _('(WARNING: executable not found)') or
linter.linter.get('comment', ''))))) linter.linter.get('comment', '')))))
# ___ _ _ _ _ __ __ _ _ # ___ _ _ _ _ __ __ _ _
# / __|___| |_ | (_)__| |_ ___ / _| / _(_) |___ ___ # / __|___| |_ | (_)__| |_ ___ / _| / _(_) |___ ___
# | (_ / -_) _| | | (_-< _| / _ \ _| | _| | / -_|_-< # | (_ / -_) _| | | (_-< _| / _ \ _| | _| | / -_|_-<
# \___\___|\__| |_|_/__/\__| \___/_| |_| |_|_\___/__/ # \___\___|\__| |_|_/__/\__| \___/_| |_| |_|_\___/__/
# #
def get_filelist(cmdline, extras): def get_filelist(cmdline, extras):
""" Returns the list of files against which we'll run the linters. """ """ Returns the list of files against which we'll run the linters. """
def base_file_filter(files): def base_file_filter(files):
""" Return the full path for all files """ """ Return the full path for all files """
return [os.path.join(git_base, file) for file in files] return [os.path.join(git_base, file) for file in files]
def cwd_file_filter(files): def cwd_file_filter(files):
""" Return the full path for only those files in the cwd and down """ """ Return the full path for only those files in the cwd and down """
gitcwd = os.path.join(os.path.relpath(os.getcwd(), git_base), '') gitcwd = os.path.join(os.path.relpath(os.getcwd(), git_base), '')
return base_file_filter([file for file in files return base_file_filter([file for file in files
if file.startswith(gitcwd)]) if file.startswith(gitcwd)])
def check_for_conflicts(filesets): def check_for_conflicts(filesets):
""" Scan list of porcelain files for merge conflic state. """ """ Scan list of porcelain files for merge conflic state. """
MERGE_CONFLICT_PAIRS = set(['DD', 'DU', 'AU', 'AA', 'UD', 'UA', 'UU']) MERGE_CONFLICT_PAIRS = set(['DD', 'DU', 'AU', 'AA', 'UD', 'UA', 'UU'])
@ -356,7 +369,6 @@ def get_filelist(cmdline, extras):
_('Current repository contains merge conflicts. Linters will not be run.')) _('Current repository contains merge conflicts. Linters will not be run.'))
return filesets return filesets
def remove_submodules(files): def remove_submodules(files):
""" Remove all submodules from the list of files git-lint cares about. """ """ Remove all submodules from the list of files git-lint cares about. """
@ -366,7 +378,6 @@ def get_filelist(cmdline, extras):
for submodule in submodules] for submodule in submodules]
return [file for file in files if (file not in submodule_names)] return [file for file in files if (file not in submodule_names)]
def get_porcelain_status(): def get_porcelain_status():
""" Return the status of all files in the system. """ """ Return the status of all files in the system. """
cmd = ['status', '-z', '--porcelain', cmd = ['status', '-z', '--porcelain',
@ -393,23 +404,20 @@ def get_filelist(cmdline, extras):
return check_for_conflicts(parse_stream([], stream)) return check_for_conflicts(parse_stream([], stream))
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 """
return [filename for (index, workspace, filename) in get_porcelain_status() return [filename for (index, workspace, filename) in get_porcelain_status()
if index in ['A', 'M']] if index in ['A', 'M']]
def working_list(): def working_list():
""" Return the list of files that have been modified in the workspace. """ Return the list of files that have been modified in the workspace.
Includes the '?' to include files that git is not currently tracking. Includes the '?' to include files that git is not currently tracking.
""" """
return [filename for (index, workspace, filename) in get_porcelain_status() return [filename for (index, workspace, filename) in get_porcelain_status()
if workspace in ['A', 'M', '?']] if workspace in ['A', 'M', '?']]
def all_list(): def all_list():
""" Return all the files git is currently tracking for this repository. """ """ Return all the files git is currently tracking for this repository. """
cmd = ['ls-tree', '--name-only', '--full-tree', '-r', '-z', git_head] cmd = ['ls-tree', '--name-only', '--full-tree', '-r', '-z', git_head]
@ -421,25 +429,25 @@ def get_filelist(cmdline, extras):
extras_fullpathed = set([os.path.abspath(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)])
return ([os.path.relpath(f, cwd) for f in (extras_fullpathed - not_found)], not_found) return ([os.path.relpath(f, cwd) for f in (extras_fullpathed - not_found)], 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:
working_directory_trans = base_file_filter working_directory_trans = base_file_filter
file_list_generator = working_list file_list_generator = working_list
if 'all' in keys: if 'all' in cmdline:
file_list_generator = all_list file_list_generator = all_list
if 'staging' in keys: if 'staging' in cmdline:
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())), [])
# ___ _ _ # ___ _ _
# / __| |_ __ _ __ _(_)_ _ __ _ __ __ ___ _ __ _ _ __ _ __ ___ _ _ # / __| |_ __ _ __ _(_)_ _ __ _ __ __ ___ _ __ _ _ __ _ __ ___ _ _
# \__ \ _/ _` / _` | | ' \/ _` | \ V V / '_/ _` | '_ \ '_ \/ -_) '_| # \__ \ _/ _` / _` | | ' \/ _` | \ V V / '_/ _` | '_ \ '_ \/ -_) '_|
# |___/\__\__,_\__, |_|_||_\__, | \_/\_/|_| \__,_| .__/ .__/\___|_| # |___/\__\__,_\__, |_|_||_\__, | \_/\_/|_| \__,_| .__/ .__/\___|_|
# |___/ |___/ |_| |_| # |___/ |___/ |_| |_|
def pick_stash_runner(cmdline): def pick_stash_runner(cmdline):
"""Choose a runner. """Choose a runner.
@ -467,12 +475,12 @@ def pick_stash_runner(cmdline):
os.utime(filename, timepair) os.utime(filename, timepair)
return results return results
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
# ___ _ _ _ # ___ _ _ _
# | _ \_ _ _ _ | (_)_ _| |_ _ __ __ _ ______ # | _ \_ _ _ _ | (_)_ _| |_ _ __ __ _ ______
# | / || | ' \ | | | ' \ _| | '_ \/ _` (_-<_-< # | / || | ' \ | | | ' \ _| | '_ \/ _` (_-<_-<
@ -492,21 +500,19 @@ def run_external_linter(filename, linter, linter_name):
return ['{}{}'.format(prefix, line) return ['{}{}'.format(prefix, line)
for line in messages.splitlines()] for line in messages.splitlines()]
cmd = linter['command'] + ' "' + filename + '"' cmd = linter['command'] + ' "' + filename + '"'
(out, err, returncode) = get_shell_response(cmd) (out, err, returncode) = get_shell_response(cmd)
failed = ((out and (linter.get('condition', 'error') == 'output')) or err or (not (returncode == 0))) failed = ((out and (linter.get('condition', 'error') == 'output')) or err or (not (returncode == 0)))
trimmed_filename = filename.replace(git_base + '/', '', 1)
if not failed: if not failed:
return (filename, linter_name, 0, []) return (trimmed_filename, linter_name, 0, [])
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 [])
return (filename, linter_name, (returncode or 1), output)
prefix = ((linter.get('print', 'false').strip().lower() != 'true') and ' ') or ' {}: '.format(trimmed_filename)
output = base_file_cleaner(encode_shell_messages(prefix, out) + ((err and encode_shell_messages(prefix, err)) or []))
return (trimmed_filename, linter_name, (returncode or 1), output)
def run_one_linter(linter, filenames): def run_one_linter(linter, filenames):
""" Runs one linter against a set of files """ Runs one linter against a set of files
Creates a match filter for the linter, extract the files to be Creates a match filter for the linter, extract the files to be
@ -514,10 +520,9 @@ def run_one_linter(linter, filenames):
result as a list of successes and failures. Failures have a result as a list of successes and failures. Failures have a
return code and the output of the lint process. return code and the output of the lint process.
""" """
linter_name, config = list(linter.items()).pop() match_filter = make_match_filter([linter])
match_filter = make_match_filter(linter)
files = set([file for file in filenames if match_filter(file)]) files = set([file for file in filenames if match_filter(file)])
return [run_external_linter(file, config, linter_name) for file in files] return [run_external_linter(file, linter.linter, linter.name) for file in files]
def build_lint_runner(linters, filenames): def build_lint_runner(linters, filenames):
@ -528,9 +533,8 @@ def build_lint_runner(linters, filenames):
runner to better handle stashing and restoring a staged commit. runner to better handle stashing and restoring a staged commit.
""" """
def lint_runner(): def lint_runner():
keys = sorted(linters.keys())
return reduce(operator.add, return reduce(operator.add,
[run_one_linter({key: linters[key]}, filenames) for key in keys], []) [run_one_linter(linter, filenames) for linter in linters], [])
return lint_runner return lint_runner
@ -540,6 +544,19 @@ def build_lint_runner(linters, filenames):
# |_| |_\__,_|_|_||_| # |_| |_\__,_|_|_||_|
# #
def print_report(results, config):
sort_position = 1
grouping = 'Linter: {}'
if 'byfile' in config:
sort_position = 0
grouping = 'Filename: {}'
grouped_results = group_by(results, sort_position)
for group in grouped_results:
print(grouping.format(group[0]))
for (filename, lintername, returncode, text) in group[1]:
print("\n".join(text))
print("")
def run_gitlint(cmdline, config, extras): def run_gitlint(cmdline, config, extras):
@ -548,23 +565,26 @@ def run_gitlint(cmdline, config, extras):
return [item for item in config if item.name in keys] return [item for item in config if item.name in keys]
""" Runs the requested linters """ """ Runs the requested linters """
(all_filenames, unfindable_filenames) = get_filelist(cmdline, extras) all_filenames, unfindable_filenames = get_filelist(cmdline, extras)
stash_runner = pick_stash_runner(cmdline) stash_runner = pick_stash_runner(cmdline)
is_lintable = make_match_filter(config) is_lintable = make_match_filter(config)
lintable_filenames = set([filename for filename in all_filenames if is_lintable(filename)]) lintable_filenames = set([filename for filename in all_filenames
if is_lintable(filename)])
unlintable_filenames = set(all_filenames) - lintable_filenames unlintable_filenames = set(all_filenames) - lintable_filenames
working_linter_names = get_working_linter_names(config) working_linter_names, broken_linter_names = get_linter_status(config)
broken_linter_names = (set([i.name for i in config]) - set(working_linter_names)) cant_lint_filter = make_match_filter(build_config_subset(
cant_lint_filter = make_match_filter(build_config_subset(broken_linter_names)) broken_linter_names))
cant_lint_filenames = [filename for filename in lintable_filenames if cant_lint_filter(filename)] cant_lint_filenames = [filename for filename in lintable_filenames
if cant_lint_filter(filename)]
lint_runner = build_lint_runner(build_config_subset(working_linters), lintable_files) lint_runner = build_lint_runner(
build_config_subset(working_linter_names), sorted(lintable_filenames))
results = stash_runner(lint_runner) results = stash_runner(lint_runner)
print(list(results)) print_report(results, cmdline)
return max([i[2] for i in results if len(i)]) return max([i[2] for i in results if len(i)])