Merge pull request #84 from pre-commit/merge_conflict_files_only_31 · precommit/pre-commit@9c35a11 · GitHub
Skip to content

Commit 9c35a11

Browse files
committed
Merge pull request pre-commit#84 from pre-commit/merge_conflict_files_only_31
Merge conflict files only 31
2 parents db6e1af + 3ebf976 commit 9c35a11

7 files changed

Lines changed: 130 additions & 37 deletions

File tree

pre_commit/commands.py

Lines changed: 2 additions & 0 deletions

pre_commit/git.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools
2+
import logging
23
import os
34
import os.path
45
import re
@@ -7,7 +8,11 @@
78
from pre_commit.util import memoize_by_cwd
89

910

10-
def _get_root_new():
11+
logger = logging.getLogger('pre_commit')
12+
13+
14+
@memoize_by_cwd
15+
def get_root():
1116
path = os.getcwd()
1217
while len(path) > 1:
1318
if os.path.exists(os.path.join(path, '.git')):
@@ -17,14 +22,33 @@ def _get_root_new():
1722
raise AssertionError('called from outside of the gits')
1823

1924

20-
@memoize_by_cwd
21-
def get_root():
22-
return _get_root_new()
25+
def is_in_merge_conflict():
26+
return os.path.exists(os.path.join('.git', 'MERGE_MSG'))
27+
28+
29+
def parse_merge_msg_for_conflicts(merge_msg):
30+
# Conflicted files start with tabs
31+
return [
32+
line.strip() for line in merge_msg.splitlines() if line.startswith('\t')
33+
]
2334

2435

25-
def get_head_sha(git_repo_path):
26-
with local.cwd(git_repo_path):
27-
return local['git']['rev-parse', 'HEAD']().strip()
36+
@memoize_by_cwd
37+
def get_conflicted_files():
38+
logger.info('Checking merge-conflict files only.')
39+
# Need to get the conflicted files from the MERGE_MSG because they could
40+
# have resolved the conflict by choosing one side or the other
41+
merge_msg = open(os.path.join('.git', 'MERGE_MSG')).read()
42+
merge_conflict_filenames = parse_merge_msg_for_conflicts(merge_msg)
43+
44+
# This will get the rest of the changes made after the merge.
45+
# If they resolved the merge conflict by choosing a mesh of both sides
46+
# this will also include the conflicted files
47+
tree_hash = local['git']['write-tree']().strip()
48+
merge_diff_filenames = local['git'][
49+
'diff', '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only',
50+
]().splitlines()
51+
return set(merge_conflict_filenames) | set(merge_diff_filenames)
2852

2953

3054
@memoize_by_cwd
@@ -57,3 +81,4 @@ def wrapper(include_expr, exclude_expr):
5781

5882
get_staged_files_matching = get_files_matching(get_staged_files)
5983
get_all_files_matching = get_files_matching(get_all_files)
84+
get_conflicted_files_matching = get_files_matching(get_conflicted_files)

testing/util.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import os.path
44
import shutil
5+
from plumbum import local
56

67

78
TESTING_DIR = os.path.abspath(os.path.dirname(__file__))
@@ -29,6 +30,11 @@ def copy_tree_to_path(src_dir, dest_dir):
2930
shutil.copy(srcname, destname)
3031

3132

33+
def get_head_sha(dir):
34+
with local.cwd(dir):
35+
return local['git']['rev-parse', 'HEAD']().strip()
36+
37+
3238
def is_valid_according_to_schema(obj, schema):
3339
try:
3440
jsonschema.validate(obj, schema)

tests/commands_test.py

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@
99
from asottile.yaml import ordered_dump
1010
from plumbum import local
1111

12-
1312
import pre_commit.constants as C
1413
from pre_commit import commands
15-
from pre_commit import git
1614
from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA
1715
from pre_commit.clientlib.validate_config import validate_config_extra
1816
from pre_commit.jsonschema_extensions import apply_defaults
1917
from pre_commit.runner import Runner
2018
from testing.auto_namedtuple import auto_namedtuple
19+
from testing.util import get_head_sha
2120
from testing.util import get_resource_path
2221

2322

@@ -53,7 +52,7 @@ def test_uninstall(empty_git_dir):
5352
def up_to_date_repo(python_hooks_repo):
5453
config = OrderedDict((
5554
('repo', python_hooks_repo),
56-
('sha', git.get_head_sha(python_hooks_repo)),
55+
('sha', get_head_sha(python_hooks_repo)),
5756
('hooks', [OrderedDict((('id', 'foo'), ('files', '')))]),
5857
))
5958
wrapped_config = apply_defaults([config], CONFIG_JSON_SCHEMA)
@@ -90,14 +89,14 @@ def test_autoupdate_up_to_date_repo(up_to_date_repo):
9089
def out_of_date_repo(python_hooks_repo):
9190
config = OrderedDict((
9291
('repo', python_hooks_repo),
93-
('sha', git.get_head_sha(python_hooks_repo)),
92+
('sha', get_head_sha(python_hooks_repo)),
9493
('hooks', [OrderedDict((('id', 'foo'), ('files', '')))]),
9594
))
9695
config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA)
9796
validate_config_extra(config_wrapped)
9897
config = config_wrapped[0]
9998
local['git']['commit', '--allow-empty', '-m', 'foo']()
100-
head_sha = git.get_head_sha(python_hooks_repo)
99+
head_sha = get_head_sha(python_hooks_repo)
101100

102101
with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj:
103102
file_obj.write(
@@ -136,7 +135,7 @@ def test_autoupdate_out_of_date_repo(out_of_date_repo):
136135
def hook_disappearing_repo(python_hooks_repo):
137136
config = OrderedDict((
138137
('repo', python_hooks_repo),
139-
('sha', git.get_head_sha(python_hooks_repo)),
138+
('sha', get_head_sha(python_hooks_repo)),
140139
('hooks', [OrderedDict((('id', 'foo'), ('files', '')))]),
141140
))
142141
config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA)
@@ -284,26 +283,6 @@ def test_has_unmerged_paths(output, expected):
284283
assert commands._has_unmerged_paths(mock_runner) is expected
285284

286285

287-
@pytest.yield_fixture
288-
def in_merge_conflict(repo_with_passing_hook):
289-
local['git']['add', C.CONFIG_FILE]()
290-
local['git']['commit', '-m' 'add hooks file']()
291-
local['git']['clone', '.', 'foo']()
292-
with local.cwd('foo'):
293-
local['git']['checkout', 'origin/master', '-b', 'foo']()
294-
with open('conflict_file', 'w') as conflict_file:
295-
conflict_file.write('herp\nderp\n')
296-
local['git']['add', 'conflict_file']()
297-
local['git']['commit', '-m', 'conflict_file']()
298-
local['git']['checkout', 'origin/master', '-b', 'bar']()
299-
with open('conflict_file', 'w') as conflict_file:
300-
conflict_file.write('harp\nddrp\n')
301-
local['git']['add', 'conflict_file']()
302-
local['git']['commit', '-m', 'conflict_file']()
303-
local['git']['merge', 'foo'](retcode=None)
304-
yield os.path.join(repo_with_passing_hook, 'foo')
305-
306-
307286
def test_merge_conflict(in_merge_conflict):
308287
ret, printed = _do_run(in_merge_conflict, _get_opts())
309288
assert ret == 1

tests/conftest.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
from __future__ import absolute_import
22

3+
import os
4+
import os.path
35
import pytest
46
import time
57
import yaml
68
from plumbum import local
79

810
import pre_commit.constants as C
9-
from pre_commit import git
1011
from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA
1112
from pre_commit.clientlib.validate_config import validate_config_extra
1213
from pre_commit.jsonschema_extensions import apply_defaults
1314
from testing.util import copy_tree_to_path
15+
from testing.util import get_head_sha
1416
from testing.util import get_resource_path
1517

1618

@@ -78,7 +80,7 @@ def failing_hook_repo(dummy_git_repo):
7880
def _make_config(path, hook_id, file_regex):
7981
config = {
8082
'repo': path,
81-
'sha': git.get_head_sha(path),
83+
'sha': get_head_sha(path),
8284
'hooks': [{'id': hook_id, 'files': file_regex}],
8385
}
8486
config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA)
@@ -126,3 +128,29 @@ def repo_with_passing_hook(config_for_script_hooks_repo, empty_git_dir):
126128
def repo_with_failing_hook(failing_hook_repo, empty_git_dir):
127129
_make_repo_from_configs(_make_config(failing_hook_repo, 'failing_hook', ''))
128130
yield empty_git_dir
131+
132+
133+
@pytest.yield_fixture
134+
def in_merge_conflict(repo_with_passing_hook):
135+
local['git']['add', C.CONFIG_FILE]()
136+
local['git']['commit', '-m' 'add hooks file']()
137+
local['git']['clone', '.', 'foo']()
138+
with local.cwd('foo'):
139+
local['git']['checkout', 'origin/master', '-b', 'foo']()
140+
with open('conflict_file', 'w') as conflict_file:
141+
conflict_file.write('herp\nderp\n')
142+
local['git']['add', 'conflict_file']()
143+
with open('foo_only_file', 'w') as foo_only_file:
144+
foo_only_file.write('foo')
145+
local['git']['add', 'foo_only_file']()
146+
local['git']['commit', '-m', 'conflict_file']()
147+
local['git']['checkout', 'origin/master', '-b', 'bar']()
148+
with open('conflict_file', 'w') as conflict_file:
149+
conflict_file.write('harp\nddrp\n')
150+
local['git']['add', 'conflict_file']()
151+
with open('bar_only_file', 'w') as bar_only_file:
152+
bar_only_file.write('bar')
153+
local['git']['add', 'bar_only_file']()
154+
local['git']['commit', '-m', 'conflict_file']()
155+
local['git']['merge', 'foo'](retcode=None)
156+
yield os.path.join(repo_with_passing_hook, 'foo')

tests/git_test.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ def test_get_root(empty_git_dir):
1414
assert git.get_root() == empty_git_dir
1515

1616

17+
def test_is_in_merge_conflict(empty_git_dir):
18+
assert git.is_in_merge_conflict() is False
19+
20+
21+
def test_is_not_in_merge_conflict(in_merge_conflict):
22+
assert git.is_in_merge_conflict() is True
23+
24+
1725
@pytest.fixture
1826
def get_files_matching_func():
1927
def get_filenames():
@@ -57,3 +65,48 @@ def test_does_not_include_deleted_fileS(get_files_matching_func):
5765
def test_exclude_removes_files(get_files_matching_func):
5866
ret = get_files_matching_func('', '\\.py$')
5967
assert ret == set(['hooks.yaml'])
68+
69+
70+
def resolve_conflict():
71+
with open('conflict_file', 'w') as conflicted_file:
72+
conflicted_file.write('herp\nderp\n')
73+
local['git']['add', 'conflict_file']()
74+
75+
76+
def test_get_conflicted_files(in_merge_conflict):
77+
resolve_conflict()
78+
with open('other_file', 'w') as other_file:
79+
other_file.write('oh hai')
80+
local['git']['add', 'other_file']()
81+
82+
ret = set(git.get_conflicted_files())
83+
assert ret == set(('conflict_file', 'other_file'))
84+
85+
86+
def test_get_conflicted_files_unstaged_files(in_merge_conflict):
87+
# If they for whatever reason did pre-commit run --no-stash during a
88+
# conflict
89+
resolve_conflict()
90+
91+
# Make unstaged file.
92+
with open('bar_only_file', 'w') as bar_only_file:
93+
bar_only_file.write('new contents!\n')
94+
95+
ret = set(git.get_conflicted_files())
96+
assert ret == set(('conflict_file',))
97+
98+
99+
MERGE_MSG = "Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n"
100+
OTHER_MERGE_MSG = MERGE_MSG + '\tother_conflict_file\n'
101+
102+
103+
@pytest.mark.parametrize(
104+
('input', 'expected_output'),
105+
(
106+
(MERGE_MSG, ['conflict_file']),
107+
(OTHER_MERGE_MSG, ['conflict_file', 'other_conflict_file']),
108+
),
109+
)
110+
def test_parse_merge_msg_for_conflicts(input, expected_output):
111+
ret = git.parse_merge_msg_for_conflicts(input)
112+
assert ret == expected_output

tests/repository_test.py

Lines changed: 2 additions & 2 deletions

0 commit comments

Comments
 (0)