diff --git a/git_lint/git_lint.py b/git_lint/git_lint.py index 40a96af..4f4212b 100644 --- a/git_lint/git_lint.py +++ b/git_lint/git_lint.py @@ -1 +1,451 @@ -# -*- coding: utf-8 -*- +from hy.core.language import filter, is_integer, map, reduce +import ConfigParser +import os +import subprocess +import operator +import re +import gettext +import sys +import getopt +sys.path.append(u'Users/ksternberg/build/git-lint/git_lint_src') +from git_lint_options import hyopt +from git_lint_config import get_config +_ = gettext.gettext +VERSION = u'0.0.2' + +def tap(a): + print(u'TAP:', a) + return a +optlist = [[u'o', u'only', True, _(u'A comma-separated list of only those linters to run'), [u'exclude']], [u'x', u'exclude', True, _(u'A comma-separated list of linters to skip'), []], [u'l', u'linters', False, _(u'Show the list of configured linters')], [u'b', u'base', False, _(u'Check all changed files in the repository, not just those in the current directory.'), []], [u'a', u'all', False, _(u'Scan all files in the repository, not just those that have changed.')], [u'e', u'every', False, _(u'Short for -b -a: scan everything')], [u'w', u'workspace', False, _(u'Scan the workspace'), [u'staging']], [u's', u'staging', False, _(u'Scan the staging area (useful for pre-commit).'), []], [u'g', u'changes', False, _(u"Report lint failures only for diff'd sections"), [u'complete']], [u'p', u'complete', False, _(u'Report lint failures for all files'), []], [u'c', u'config', True, _(u'Path to config file'), []], [u'h', u'help', False, _(u'This help message'), []], [u'v', u'version', False, _(u'Version information'), []]] + +def get_git_response_raw(cmd): + + def _hy_anon_fn_2(): + fullcmd = ([u'git'] + cmd) + process = subprocess.Popen(fullcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = process.communicate() + (out, err) + return (out, err, process.returncode) + return _hy_anon_fn_2() + +def get_git_response(cmd): + + def _hy_anon_fn_4(): + (out, error, returncode) = get_git_response_raw(cmd) + (out, error, returncode) + return out + return _hy_anon_fn_4() + +def split_git_response(cmd): + + def _hy_anon_fn_6(): + (out, error, returncode) = get_git_response_raw(cmd) + (out, error, returncode) + return out.splitlines() + return _hy_anon_fn_6() + +def run_git_command(cmd): + + def _hy_anon_fn_8(): + fullcmd = ([u'git'] + cmd) + return subprocess.call(fullcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return _hy_anon_fn_8() + +def get_shell_response(fullcmd): + + def _hy_anon_fn_10(): + process = subprocess.Popen(fullcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + (out, err) = process.communicate() + (out, err) + return (out, err, process.returncode) + return _hy_anon_fn_10() + +def _hy_anon_fn_12(): + (out, error, returncode) = get_git_response_raw([u'rev-parse', u'--show-toplevel']) + (out, error, returncode) + return (None if (not (returncode == 0L)) else out.rstrip()) +git_base = _hy_anon_fn_12() + +def _hy_anon_fn_13(): + empty_repository_hash = u'4b825dc642cb6eb9a060e54bf8d69288fbee4904' + (out, err, returncode) = get_git_response_raw([u'rev-parse', u'--verify HEAD']) + (out, err, returncode) + return (u'HEAD' if (not err) else empty_repository_hash) +git_head = _hy_anon_fn_13() + +def run_external_checker(path, config): + + def _hy_anon_fn_15(): + cmd = config[u'command'].format((command + u' "{}"'), path) + (out, err, returncode) = get_shell_response(cmd) + (out, err, returncode) + if ((out and (check.get(u'error_condition', u'error') == u'output')) or err or (not (returncode == 0L))): + + def _hy_anon_fn_14(): + prefix = (u'\t{}:'.format(filename) if check[u'print_filename'] else u'\t') + output = (encode_shell_messages(prefix, out) + (encode_shell_messages(prefix, err) if err else [])) + return [(returncode or 1L), output] + _hy_anon_var_1 = _hy_anon_fn_14() + else: + _hy_anon_var_1 = [0L, []] + return _hy_anon_var_1 + return _hy_anon_fn_15() + +def make_match_filter_matcher(extensions): + + def _hy_anon_fn_17(s): + return re.compile(s, re.I) + + def _hy_anon_fn_18(s): + return ((u'\\.(' + s) + u')$') + + def _hy_anon_fn_19(s): + return re.sub(u'^\\.', u'', s) + + def _hy_anon_fn_20(s): + return (not (0L == len(s))) + + def _hy_anon_fn_21(s): + return s.strip() + + def _hy_anon_fn_22(s): + return s.split(u',') + return _hy_anon_fn_17(_hy_anon_fn_18(u'|'.join(map(_hy_anon_fn_19, filter(_hy_anon_fn_20, set(map(_hy_anon_fn_21, reduce(operator.add, map(_hy_anon_fn_22, extensions))))))))) + +def make_match_filter(config): + + def _hy_anon_fn_26(): + + def _hy_anon_fn_24(v): + return v.get(u'match', u'') + matcher = make_match_filter_matcher(map(_hy_anon_fn_24, config.itervalues())) + + def _hy_anon_fn_25(path): + return matcher.search(path) + return _hy_anon_fn_25 + return _hy_anon_fn_26() + +def executable_exists(script, label): + if (not len(script)): + _hy_anon_var_4 = sys.exit(_(u'Syntax error in command configuration for {} ').format(label)) + else: + + def _hy_anon_fn_31(): + scriptname = script.split(u' ')[0L] + paths = os.environ.get(u'PATH').split(u':') + + def isexecutable(p): + return (os.path.exists(p) and os.access(p, os.X_OK)) + if (not len(scriptname)): + _hy_anon_var_3 = sys.exit(_(u'Syntax error in command configuration for {} ').format(label)) + else: + if (scriptname[0L] == u'/'): + _hy_anon_var_2 = (scriptname if isexecutable(scriptname) else None) + else: + + def _hy_anon_fn_30(): + + def _hy_anon_fn_29(path): + return isexecutable(os.path.join(path, scriptname)) + possibles = list(filter(_hy_anon_fn_29, paths)) + return (possibles[0L] if len(possibles) else None) + _hy_anon_var_2 = _hy_anon_fn_30() + _hy_anon_var_3 = _hy_anon_var_2 + return _hy_anon_var_3 + _hy_anon_var_4 = _hy_anon_fn_31() + return _hy_anon_var_4 + +def get_working_linters(config): + + def _hy_anon_fn_34(): + + def found(key): + return executable_exists(config.get(key).get(u'command'), key) + return set(filter(found, config.keys())) + return _hy_anon_fn_34() + +def print_linters(config): + print(_(u'Currently supported linters:')) + + def _hy_anon_fn_36(): + working = get_working_linters(config) + broken = (set(config.keys()) - working) + for key in sorted(working): + print(u'{:<14} {}'.format(key, config.get(key).get(u'comment', u''))) + for key in sorted(broken): + print(u'{:<14} {}'.format(key, _(u'(WARNING: executable not found)'))) + return _hy_anon_fn_36() + +def base_file_filter(files): + + def _hy_anon_fn_38(f): + return os.path.join(git_base, f) + return map(_hy_anon_fn_38, files) + +def cwd_file_filter(files): + + def _hy_anon_fn_41(): + gitcwd = os.path.join(os.path.relpath(os.getcwd(), git_base), u'') + + def _hy_anon_fn_40(f): + return f.startswith(gitcwd) + return base_file_filter(filter(_hy_anon_fn_40, files)) + return _hy_anon_fn_41() + +def base_file_cleaner(files): + + def _hy_anon_fn_43(f): + return f.replace(git_base, 1L) + return map(_hy_anon_fn_43, files) +MERGE_CONFLICT_PAIRS = set([u'DD', u'DU', u'AU', u'AA', u'UD', u'UA', u'UU']) + +def check_for_conflicts(files): + + def _hy_anon_fn_46(): + + def _hy_anon_fn_45(_hy_anon_var_5): + (index, workspace, filename) = _hy_anon_var_5 + (index, workspace, filename) + return ((u'' + index) + workspace) + status_pairs = map(_hy_anon_fn_45, files) + conflicts = (set(MERGE_CONFLICT_PAIRS) & set(status_pairs)) + return (sys.exit(_(u'Current repository contains merge conflicts. Linters will not be run.')) if len(conflicts) else files) + return _hy_anon_fn_46() + +def remove_submodules(files): + + def _hy_anon_fn_52(): + + def split_out_paths(s): + return s.split(u' ')[2L] + fixer_re = re.compile(u'^(\\.\\.\\/)+') + + def fixer_to_base(s): + return fixer_re.sub(u'', s) + submodule_entries = split_git_response([u'submodule', u'status']) + + def _hy_anon_fn_50(s): + return fixer_to_base(split_out_paths(s)) + submodule_names = map(_hy_anon_fn_50, submodule_entries) + + def _hy_anon_fn_51(s): + return (not (s in submodule_names)) + return filter(_hy_anon_fn_51, files) + return _hy_anon_fn_52() + +def get_porcelain_status(): + + def _hy_anon_fn_57(): + cmd = [u'status', u'-z', u'--porcelain', u'--untracked-files=all', u'--ignore-submodules=all'] + + def nonnull(s): + return (len(s) > 0L) + stream = list(filter(nonnull, get_git_response(cmd).split(u'\x00'))) + + def parse_stream(acc, stream): + if (0L == len(stream)): + _hy_anon_var_6 = acc + else: + + def _hy_anon_fn_55(): + temp = stream.pop(0L) + index = temp[0L] + workspace = temp[1L] + filename = temp[3L:] + (stream.pop(0L) if (index == u'R') else None) + return parse_stream((acc + [(index, workspace, filename)]), stream) + _hy_anon_var_6 = _hy_anon_fn_55() + return _hy_anon_var_6 + return check_for_conflicts(parse_stream([], stream)) + return _hy_anon_fn_57() + +def staging_list(): + + def _hy_anon_fn_59(_hy_anon_var_7): + (index, workspace, filename) = _hy_anon_var_7 + (index, workspace, filename) + return filename + + def _hy_anon_fn_60(_hy_anon_var_8): + (index, workspace, filename) = _hy_anon_var_8 + (index, workspace, filename) + return (index in [u'A', u'M']) + return map(_hy_anon_fn_59, filter(_hy_anon_fn_60, get_porcelain_status())) + +def working_list(): + + def _hy_anon_fn_62(_hy_anon_var_9): + (index, workspace, filename) = _hy_anon_var_9 + (index, workspace, filename) + return filename + + def _hy_anon_fn_63(_hy_anon_var_10): + (index, workspace, filename) = _hy_anon_var_10 + (index, workspace, filename) + return (workspace in [u'A', u'M', u'?']) + return map(_hy_anon_fn_62, filter(_hy_anon_fn_63, get_porcelain_status())) + +def all_list(): + + def _hy_anon_fn_66(): + cmd = [u'ls-tree', u'--name-only', u'--full-tree', u'-r', u'-z', git_head] + + def _hy_anon_fn_65(s): + return (len(s) > 0L) + return filter(_hy_anon_fn_65, get_git_response(cmd).split(u'\x00')) + return _hy_anon_fn_66() + +def get_filelist(options): + + def _hy_anon_fn_69(): + keys = options.keys() + working_directory_trans = (base_file_filter if len((set(keys) & set([u'base', u'every']))) else cwd_file_filter) + file_list_generator = (staging_list if (u'staging' in keys) else (all_list if (u'all' in keys) else (working_list if True else None))) + + def _hy_anon_fn_68(): + return working_directory_trans(remove_submodules(file_list_generator())) + return set(_hy_anon_fn_68()) + return _hy_anon_fn_69() + +def staging_wrapper(run_linters): + + def _hy_anon_fn_74(): + + def time_gather(f): + + def _hy_anon_fn_71(): + stats = os.stat(f) + return (f, (stats.atime, stats.mtime)) + return _hy_anon_fn_71() + times = list(map(time_gather, files)) + run_git_command([u'stash', u'--keep-index']) + + def _hy_anon_fn_73(): + results = run_linters() + run_git_command([u'reset', u'--hard']) + run_git_command([u'stash', u'pop', u'--quiet', u'--index']) + for (filename, timepair) in times: + os.utime(filename, timepair) + return results + return _hy_anon_fn_73() + return _hy_anon_fn_74() + +def workspace_wrapper(run_linters): + return run_linters() + +def pick_runner(options): + + def _hy_anon_fn_77(): + keys = options.keys() + return (staging_wrapper if (u'staging' in keys) else workspace_wrapper) + return _hy_anon_fn_77() + +def lmap(pred, iter): + return list(map(pred, iter)) + +def encode_shell_messages(prefix, messages): + + def _hy_anon_fn_80(line): + return u'{}{}'.format(prefix, line.decode(u'utf-8')) + return lmap(_hy_anon_fn_80, messages.splitlines()) + +def run_external_linter(filename, linter): + + def _hy_anon_fn_83(): + cmd = (((linter[u'command'] + u'"') + filename) + u'"') + (out, err, returncode) = get_shell_response(cmd) + (out, err, returncode) + if ((out and (linter.get(u'condition', u'error') == u'output')) or err or (not (returncode == 0L))): + + def _hy_anon_fn_82(): + prefix = (u'\t{}:'.format(filename) if linter[u'print'] else u'\t') + output = (encode_shell_messages(prefix, out) + (encode_shell_messages(prefix, err) if err else [])) + return [(returncode or 1L), output] + _hy_anon_var_11 = _hy_anon_fn_82() + else: + _hy_anon_var_11 = [0L, []] + return _hy_anon_var_11 + return _hy_anon_fn_83() + +def run_one_linter(linter, filenames): + + def _hy_anon_fn_86(): + match_filter = make_match_filter(linter) + config = linter.values()[0L] + files = set(filter(match_filter, filenames)) + + def _hy_anon_fn_85(f): + return run_external_linter(f, config) + return list(map(_hy_anon_fn_85, files)) + return _hy_anon_fn_86() + +def build_lint_runner(linters, filenames): + + def _hy_anon_fn_90(): + + def _hy_anon_fn_89(): + keys = sorted(linters.keys()) + + def _hy_anon_fn_88(key): + return run_one_linter({key: linters[key], }, filenames) + return map(_hy_anon_fn_88, keys) + return _hy_anon_fn_89() + return _hy_anon_fn_90 + +def subset_config(config, keys): + + def _hy_anon_fn_92(): + ret = {} + for item in config.items(): + if (item[0L] in keys): + ret[item[0L]] = item[1L] + _hy_anon_var_12 = None + else: + _hy_anon_var_12 = None + return ret + return _hy_anon_fn_92() + +def run_gitlint(options, config, extras): + + def _hy_anon_fn_94(): + all_files = get_filelist(options) + runner = pick_runner(options) + match_filter = make_match_filter(config) + lintable_files = set(filter(match_filter, all_files)) + unlintables = (set(all_files) - lintable_files) + working_linters = get_working_linters(config) + broken_linters = (set(config) - set(working_linters)) + cant_lint_filter = make_match_filter(subset_config(config, broken_linters)) + cant_lintable = set(filter(cant_lint_filter, lintable_files)) + lint_runner = build_lint_runner(subset_config(config, working_linters), lintable_files) + results = runner(lint_runner) + print(u'No Linter Available:', list(unlintables)) + print(u'Linter Executable Not Found for:', list(cant_lintable)) + return print(list(results)) + return _hy_anon_fn_94() + +def main(*args): + + def _hy_anon_fn_97(): + opts = hyopt(optlist, args, u'git lint', u'Copyright (c) 2008, 2016 Kenneth M. "Elf" Sternberg ', u'0.0.4') + if (git_base == None): + _hy_anon_var_14 = sys.exit(_(u'Not currently in a git repository.')) + else: + try: + + def _hy_anon_fn_96(): + options = opts.get_options() + config = get_config(options, git_base) + return (opts.print_help() if options.has_key(u'help') else (opts.print_version() if options.has_key(u'version') else (print_linters(config) if options.has_key(u'linters') else (run_gitlint(options, config, opts.filenames) if True else None)))) + _hy_anon_var_13 = _hy_anon_fn_96() + except getopt.GetoptError as err: + _hy_anon_var_13 = opts.print_help() + _hy_anon_var_14 = _hy_anon_var_13 + return _hy_anon_var_14 + return _hy_anon_fn_97() +if (__name__ == u'__main__'): + import sys + :G_1235 = main(*sys.argv) + _hy_anon_var_15 = (sys.exit(:G_1235) if is_integer(:G_1235) else None) +else: + _hy_anon_var_15 = None diff --git a/git_lint/git_lint_config.py b/git_lint/git_lint_config.py new file mode 100644 index 0000000..9472073 --- /dev/null +++ b/git_lint/git_lint_config.py @@ -0,0 +1,33 @@ +import sys +import os.path +import gettext +import ConfigParser +_ = gettext.gettext + +def _find_config_file(options, base): + if options.has_key('config'): + 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 = filter(os.path.isfile, possibles) + if len(matches) == 0: + sys.exit(_('No configuration file found'))) + + return matches[0] + +def get_config(options, base): + path = find_config_file(options, base) + configloader = ConfigParser.SafeConfigParser() + configloader.read(path) + configloader.set('DEFAULT', 'repdir', base) + return {section: {k, v for (k, v) in configloader.items(section)} + for section in configloader.sections()} diff --git a/git_lint/git_lint_options.py b/git_lint/git_lint_options.py new file mode 100644 index 0000000..47ab616 --- /dev/null +++ b/git_lint/git_lint_options.py @@ -0,0 +1,130 @@ +from hy.core.language import filter, map, name, reduce +import os +import sys +import inspect +import getopt +import gettext +_ = gettext.gettext + +def get_script_name(): + if getattr(sys, u'frozen', False): + + def _hy_anon_fn_1(): + (path, name) = os.path.split(sys.executable) + (path, name) + return name() + _hy_anon_var_1 = _hy_anon_fn_1() + else: + + def _hy_anon_fn_4(): + prefix = sys.exec_prefix.upper() + + def _hy_anon_fn_3(a): + + def _hy_anon_fn_2(): + fname = a[1L] + return (not (fname.startswith(u'<') or fname.upper().startswith(prefx))) + _hy_anon_fn_2() + return inspect.stack() + names = filter(_hy_anon_fn_3) + name = names.pop() + return name() + _hy_anon_var_1 = _hy_anon_fn_4() + return _hy_anon_var_1 + + + +def make_options_rationalizer(optlist): + """ 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. + """ + + def make_opt_assoc(prefix, pos): + def associater(acc, it): + acc[(prefix + it[pos])] = it[1] + return acc + return associater + + short_opt_assoc = make_opt_assoc(u'-', 0) + long_opt_assoc = make_opt_assoc(u'--', 1) + + def make_full_set(acc, i): + return long_opt_assoc(short_opt_assoc(acc, i), i) + + fullset = reduce(make_full_set, optlist, {}) + + def rationalizer(acc, it): + acc[fullset[it[0]]] = it[1] + return acc + + return rationalizer + + +def remove_conflicted_options(optlist, config): + keys = config.keys() + marked = filter(lambda o: o[1] in keys, optlist) + + + exclude = reduce + def _hy_anon_fn_13(memo, opt): + return (memo + (opt[4L] if (len(opt) > 4L) else [])) + exclude = reduce(_hy_anon_fn_13, marked, []) + + def _hy_anon_fn_14(key): + return (key in exclude) + excluded = filter(_hy_anon_fn_14, keys) + + def _hy_anon_fn_15(memo, key): + if (not (key in excluded)): + memo[key] = config[key] + _hy_anon_var_2 = None + else: + _hy_anon_var_2 = None + return memo + cleaned = reduce(_hy_anon_fn_15, keys, {}) + return (cleaned, excluded) + return _hy_anon_fn_16() + + +class RationalOptions: + + def __init__(self, optlist, args, name=u'', copyright=u'', version=u'0.0.1'): + def shortoptstogo(i): + return i[0] + (i[2] and ':' or '') + + def longoptstogo(i): + return i[1] + (i[2] and '=' or '') + + optstringsshort = ''.join(map(shortoptstogo, optlist)) + optstringslong = map(longoptstogo, optlist)) + (options, arg) = getopt.getopt(args[1:], optstringsshort, optstringslong) + rationalize_options = make_options_rationalizer(optlist) + + (newoptions, excluded) = remove_conflicted_options( + optlist, reduce(rationalize_options, options, {})) + + self.optlist = optlist + self.options = newoptions + self.excluded = excluded + self.filenames = arg + self.name = (name if name else get_script_name()) + self.version = version + self.copyright = copyright + + def get_options(self): + return self.options + + def get_keys(self): + return set(self.options.keys()) + + def print_help(self): + print(_('Usage: {} [options] [filenames]').format(self.name)) + for item in self.optlist: + print(' -{:<1} --{:<12} {}'.format(item[0L], item[1L], item[3L])) + return sys.exit() + + def print_version(self): + print('{}'.format(self.name, self.version)) + if self.copyright: + print(self.copyright) + return sys.exit()