from __future__ import unicode_literals import functools import logging import os.path import re import sys from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import memoize_by_cwd # octal constants for git file modes GIT_MODE_FILE = 0o100644 GIT_MODE_EXECUTABLE = 0o100755 GIT_MODE_SYMLINK = 0o120000 GIT_MODE_SUBMODULE = 0o160000 logger = logging.getLogger('pre_commit') def get_root(): try: return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' 'directory?' ) def get_git_dir(git_root): return os.path.normpath(os.path.join( git_root, cmd_output('git', 'rev-parse', '--git-dir', cwd=git_root)[1].strip(), )) def get_remote_url(git_root): ret = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root)[1] return ret.strip() def is_in_merge_conflict(): git_dir = get_git_dir('.') return ( os.path.exists(os.path.join(git_dir, 'MERGE_MSG')) and os.path.exists(os.path.join(git_dir, 'MERGE_HEAD')) ) def parse_merge_msg_for_conflicts(merge_msg): # Conflicted files start with tabs return [ line.lstrip(b'#').strip().decode('UTF-8') for line in merge_msg.splitlines() # '#\t' for git 2.4.1 if line.startswith((b'\t', b'#\t')) ] @memoize_by_cwd def get_conflicted_files(): logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other merge_msg = open(os.path.join(get_git_dir('.'), 'MERGE_MSG'), 'rb').read() merge_conflict_filenames = parse_merge_msg_for_conflicts(merge_msg) # This will get the rest of the changes made after the merge. # If they resolved the merge conflict by choosing a mesh of both sides # this will also include the conflicted files tree_hash = cmd_output('git', 'write-tree')[1].strip() merge_diff_filenames = cmd_output( 'git', 'diff', '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only', )[1].splitlines() return set(merge_conflict_filenames) | set(merge_diff_filenames) @memoize_by_cwd def get_staged_files(): return cmd_output( 'git', 'diff', '--staged', '--name-only', # Everything except for D '--diff-filter=ACMRTUXB' )[1].splitlines() @memoize_by_cwd def get_all_files(): """Return a list of all actual files in the git repository. There are some types of content we want to exclude. In order to exclude submodules, which git tracks similarly to files, we call `git ls-files --stage` and grep out entries with the special submodule file mode. http://stackoverflow.com/a/24122304 """ # The output format of the command is: # [file mode] [object hash] [stage number]\t[file path] split_regex = re.compile('^([0-7]{6}) [0-9a-f]{40} [0-9]+\t(.+)$') def split(line): match = split_regex.match(line) return int(match.group(1), 8), match.group(2) output = cmd_output('git', 'ls-files', '--stage')[1] return [ path for mode, path in [ split(line) for line in output.splitlines() ] if mode != GIT_MODE_SUBMODULE ] def get_files_matching(all_file_list_strategy): @functools.wraps(all_file_list_strategy) @memoize_by_cwd def wrapper(include_expr, exclude_expr): include_regex = re.compile(include_expr) exclude_regex = re.compile(exclude_expr) return { filename for filename in all_file_list_strategy() if ( include_regex.search(filename) and not exclude_regex.search(filename) and os.path.lexists(filename) ) } return wrapper get_staged_files_matching = get_files_matching(get_staged_files) get_all_files_matching = get_files_matching(get_all_files) get_conflicted_files_matching = get_files_matching(get_conflicted_files) def check_for_cygwin_mismatch(): """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) is_cygwin_python = sys.platform == 'cygwin' toplevel = cmd_output('git', 'rev-parse', '--show-toplevel')[1] is_cygwin_git = toplevel.startswith('/') if is_cygwin_python ^ is_cygwin_git: exe_type = {True: '(cygwin)', False: '(windows)'} logger.warn( 'pre-commit has detected a mix of cygwin python / git\n' 'This combination is not supported, it is likely you will ' 'receive an error later in the program.\n' 'Make sure to use cygwin git+python while using cygwin\n' 'These can be installed through the cygwin installer.\n' ' - python {}\n' ' - git {}\n'.format( exe_type[is_cygwin_python], exe_type[is_cygwin_git], ) )