From 5810ee43152fcdf1d09a967f87bd79b700512621 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 14 Apr 2014 22:50:16 -0700 Subject: [PATCH] Implement merge-files only using @bukzor's method. --- pre_commit/commands.py | 2 ++ pre_commit/git.py | 35 +++++++++++++++++++++++++--- tests/commands_test.py | 20 ---------------- tests/conftest.py | 28 ++++++++++++++++++++++ tests/git_test.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 23 deletions(-) diff --git a/pre_commit/commands.py b/pre_commit/commands.py index 33e36879..d9cb0d4e 100644 --- a/pre_commit/commands.py +++ b/pre_commit/commands.py @@ -147,6 +147,8 @@ def clean(runner): def _run_single_hook(runner, repository, hook_id, args, write): if args.all_files: get_filenames = git.get_all_files_matching + elif git.is_in_merge_conflict(): + get_filenames = git.get_conflicted_files_matching else: get_filenames = git.get_staged_files_matching diff --git a/pre_commit/git.py b/pre_commit/git.py index bd6eb3d7..79507680 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,4 +1,5 @@ import functools +import logging import os import os.path import re @@ -7,7 +8,11 @@ from plumbum import local from pre_commit.util import memoize_by_cwd -def _get_root_new(): +logger = logging.getLogger('pre_commit') + + +@memoize_by_cwd +def get_root(): path = os.getcwd() while len(path) > 1: if os.path.exists(os.path.join(path, '.git')): @@ -17,9 +22,32 @@ def _get_root_new(): raise AssertionError('called from outside of the gits') +def is_in_merge_conflict(): + return os.path.exists(os.path.join('.git', 'MERGE_MSG')) + + +def parse_merge_msg_for_conflicts(merge_msg): + # Conflicted files start with tabs + return [ + line.strip() for line in merge_msg.splitlines() if line.startswith('\t') + ] + + @memoize_by_cwd -def get_root(): - return _get_root_new() +def get_conflicted_files(): + # 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('.git', 'MERGE_MSG')).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 = local['git']['write-tree']().strip() + merge_diff_filenames = local['git'][ + 'diff', '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only', + ]().splitlines() + return set(merge_conflict_filenames) | set(merge_diff_filenames) @memoize_by_cwd @@ -52,3 +80,4 @@ def get_files_matching(all_file_list_strategy): 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) diff --git a/tests/commands_test.py b/tests/commands_test.py index 55cd3f15..c9f553c3 100644 --- a/tests/commands_test.py +++ b/tests/commands_test.py @@ -283,26 +283,6 @@ def test_has_unmerged_paths(output, expected): assert commands._has_unmerged_paths(mock_runner) is expected -@pytest.yield_fixture -def in_merge_conflict(repo_with_passing_hook): - local['git']['add', C.CONFIG_FILE]() - local['git']['commit', '-m' 'add hooks file']() - local['git']['clone', '.', 'foo']() - with local.cwd('foo'): - local['git']['checkout', 'origin/master', '-b', 'foo']() - with open('conflict_file', 'w') as conflict_file: - conflict_file.write('herp\nderp\n') - local['git']['add', 'conflict_file']() - local['git']['commit', '-m', 'conflict_file']() - local['git']['checkout', 'origin/master', '-b', 'bar']() - with open('conflict_file', 'w') as conflict_file: - conflict_file.write('harp\nddrp\n') - local['git']['add', 'conflict_file']() - local['git']['commit', '-m', 'conflict_file']() - local['git']['merge', 'foo'](retcode=None) - yield os.path.join(repo_with_passing_hook, 'foo') - - def test_merge_conflict(in_merge_conflict): ret, printed = _do_run(in_merge_conflict, _get_opts()) assert ret == 1 diff --git a/tests/conftest.py b/tests/conftest.py index 27f5d2f6..9c513eee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ from __future__ import absolute_import +import os +import os.path import pytest import time import yaml @@ -126,3 +128,29 @@ def repo_with_passing_hook(config_for_script_hooks_repo, empty_git_dir): def repo_with_failing_hook(failing_hook_repo, empty_git_dir): _make_repo_from_configs(_make_config(failing_hook_repo, 'failing_hook', '')) yield empty_git_dir + + +@pytest.yield_fixture +def in_merge_conflict(repo_with_passing_hook): + local['git']['add', C.CONFIG_FILE]() + local['git']['commit', '-m' 'add hooks file']() + local['git']['clone', '.', 'foo']() + with local.cwd('foo'): + local['git']['checkout', 'origin/master', '-b', 'foo']() + with open('conflict_file', 'w') as conflict_file: + conflict_file.write('herp\nderp\n') + local['git']['add', 'conflict_file']() + with open('foo_only_file', 'w') as foo_only_file: + foo_only_file.write('foo') + local['git']['add', 'foo_only_file']() + local['git']['commit', '-m', 'conflict_file']() + local['git']['checkout', 'origin/master', '-b', 'bar']() + with open('conflict_file', 'w') as conflict_file: + conflict_file.write('harp\nddrp\n') + local['git']['add', 'conflict_file']() + with open('bar_only_file', 'w') as bar_only_file: + bar_only_file.write('bar') + local['git']['add', 'bar_only_file']() + local['git']['commit', '-m', 'conflict_file']() + local['git']['merge', 'foo'](retcode=None) + yield os.path.join(repo_with_passing_hook, 'foo') diff --git a/tests/git_test.py b/tests/git_test.py index 4dd01f99..63cb1c61 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -14,6 +14,14 @@ def test_get_root(empty_git_dir): assert git.get_root() == empty_git_dir +def test_is_in_merge_conflict(empty_git_dir): + assert git.is_in_merge_conflict() is False + + +def test_is_not_in_merge_conflict(in_merge_conflict): + assert git.is_in_merge_conflict() is True + + @pytest.fixture def get_files_matching_func(): def get_filenames(): @@ -57,3 +65,48 @@ def test_does_not_include_deleted_fileS(get_files_matching_func): def test_exclude_removes_files(get_files_matching_func): ret = get_files_matching_func('', '\\.py$') assert ret == set(['hooks.yaml']) + + +def resolve_conflict(): + with open('conflict_file', 'w') as conflicted_file: + conflicted_file.write('herp\nderp\n') + local['git']['add', 'conflict_file']() + + +def test_get_conflicted_files(in_merge_conflict): + resolve_conflict() + with open('other_file', 'w') as other_file: + other_file.write('oh hai') + local['git']['add', 'other_file']() + + ret = set(git.get_conflicted_files()) + assert ret == set(('conflict_file', 'other_file')) + + +def test_get_conflicted_files_unstaged_files(in_merge_conflict): + # If they for whatever reason did pre-commit run --no-stash during a + # conflict + resolve_conflict() + + # Make unstaged file. + with open('bar_only_file', 'w') as bar_only_file: + bar_only_file.write('new contents!\n') + + ret = set(git.get_conflicted_files()) + assert ret == set(('conflict_file',)) + + +MERGE_MSG = "Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n" +OTHER_MERGE_MSG = MERGE_MSG + '\tother_conflict_file\n' + + +@pytest.mark.parametrize( + ('input', 'expected_output'), + ( + (MERGE_MSG, ['conflict_file']), + (OTHER_MERGE_MSG, ['conflict_file', 'other_conflict_file']), + ), +) +def test_parse_merge_msg_for_conflicts(input, expected_output): + ret = git.parse_merge_msg_for_conflicts(input) + assert ret == expected_output