Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4ed9120

Browse files
committed
Add staged_files_only context manager.
1 parent 7496151 commit 4ed9120

7 files changed

Lines changed: 287 additions & 9 deletions

File tree

pre_commit/prefixed_command_runner.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
class CalledProcessError(RuntimeError):
88
def __init__(self, returncode, cmd, expected_returncode, output=None):
9+
super(CalledProcessError, self).__init__(
10+
returncode, cmd, expected_returncode, output,
11+
)
912
self.returncode = returncode
1013
self.cmd = cmd
1114
self.expected_returncode = expected_returncode
@@ -15,13 +18,13 @@ def __str__(self):
1518
return (
1619
'Command: {0!r}\n'
1720
'Return code: {1}\n'
18-
'Expected return code {2}\n',
21+
'Expected return code: {2}\n'
1922
'Output: {3!r}\n'.format(
2023
self.cmd,
2124
self.returncode,
2225
self.expected_returncode,
2326
self.output,
24-
),
27+
)
2528
)
2629

2730

@@ -48,15 +51,15 @@ def _create_path_if_not_exists(self):
4851
self.__makedirs(self.prefix_dir)
4952

5053
def run(self, cmd, retcode=0, stdin=None, **kwargs):
54+
popen_kwargs = {
55+
'stdin': subprocess.PIPE,
56+
'stdout': subprocess.PIPE,
57+
'stderr': subprocess.PIPE,
58+
}
59+
popen_kwargs.update(kwargs)
5160
self._create_path_if_not_exists()
5261
replaced_cmd = _replace_cmd(cmd, prefix=self.prefix_dir)
53-
proc = self.__popen(
54-
replaced_cmd,
55-
stdin=subprocess.PIPE,
56-
stdout=subprocess.PIPE,
57-
stderr=subprocess.PIPE,
58-
**kwargs
59-
)
62+
proc = self.__popen(replaced_cmd, **popen_kwargs)
6063
stdout, stderr = proc.communicate(stdin)
6164
returncode = proc.returncode
6265

pre_commit/staged_files_only.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
import contextlib
3+
import time
4+
5+
from pre_commit.prefixed_command_runner import CalledProcessError
6+
7+
8+
@contextlib.contextmanager
9+
def staged_files_only(cmd_runner):
10+
"""Clear any unstaged changes from the git working directory inside this
11+
context.
12+
13+
Args:
14+
cmd_runner - PrefixedCommandRunner
15+
"""
16+
# Determine if there are unstaged files
17+
retcode, _, _ = cmd_runner.run(
18+
['git', 'diff-files', '--quiet'],
19+
retcode=None,
20+
)
21+
if retcode:
22+
# TODO: print a warning message that unstaged things are being stashed
23+
# Save the current unstaged changes as a patch
24+
# TODO: use a more unique patch filename
25+
patch_filename = cmd_runner.path('patch{0}'.format(time.time()))
26+
with open(patch_filename, 'w') as patch_file:
27+
cmd_runner.run(['git', 'diff', '--binary'], stdout=patch_file)
28+
29+
# Clear the working directory of unstaged changes
30+
cmd_runner.run(['git', 'checkout', '--', '.'])
31+
try:
32+
yield
33+
finally:
34+
# Try to apply the patch we saved
35+
try:
36+
cmd_runner.run(['git', 'apply', patch_filename])
37+
except CalledProcessError:
38+
# TOOD: print a warning about rolling back changes made by hooks
39+
# We failed to apply the patch, presumably due to fixes made
40+
# by hooks.
41+
# Roll back the changes made by hooks.
42+
cmd_runner.run(['git', 'checkout', '--', '.'])
43+
cmd_runner.run(['git', 'apply', patch_filename])
44+
else:
45+
# There weren't any staged files so we don't need to do anything
46+
# special
47+
yield

testing/resources/img1.jpg

843 Bytes
Loading

testing/resources/img2.jpg

891 Bytes
Loading

testing/resources/img3.jpg

859 Bytes
Loading

tests/prefixed_command_runner_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99
from pre_commit.prefixed_command_runner import PrefixedCommandRunner
1010

1111

12+
def test_CalledProcessError_str():
13+
error = CalledProcessError(1, ['git', 'status'], 0, ('stdout', 'stderr'))
14+
assert str(error) == (
15+
"Command: ['git', 'status']\n"
16+
"Return code: 1\n"
17+
"Expected return code: 0\n"
18+
"Output: ('stdout', 'stderr')\n"
19+
)
20+
21+
1222
@pytest.fixture
1323
def popen_mock():
1424
popen = mock.Mock(spec=subprocess.Popen)

tests/staged_files_only_test.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
2+
import os.path
3+
import pytest
4+
import shutil
5+
from plumbum import local
6+
7+
import pre_commit.constants as C
8+
from pre_commit.prefixed_command_runner import PrefixedCommandRunner
9+
from pre_commit.staged_files_only import staged_files_only
10+
from testing.auto_namedtuple import auto_namedtuple
11+
from testing.util import get_resource_path
12+
13+
14+
FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', ''))
15+
16+
17+
def get_short_git_status():
18+
git_status = local['git']['status', '-s']()
19+
return dict(reversed(line.split()) for line in git_status.splitlines())
20+
21+
22+
@pytest.yield_fixture
23+
def foo_staged(empty_git_dir):
24+
with open('.gitignore', 'w') as gitignore_file:
25+
gitignore_file.write(C.HOOKS_WORKSPACE + '\n')
26+
local['git']['add', '.']()
27+
local['git']['commit', '-m', 'add gitignore']()
28+
29+
with open('foo', 'w') as foo_file:
30+
foo_file.write(FOO_CONTENTS)
31+
local['git']['add', 'foo']()
32+
foo_filename = os.path.join(empty_git_dir, 'foo')
33+
yield auto_namedtuple(path=empty_git_dir, foo_filename=foo_filename)
34+
35+
36+
@pytest.fixture
37+
def cmd_runner():
38+
return PrefixedCommandRunner(C.HOOKS_WORKSPACE)
39+
40+
41+
def _test_foo_state(path, foo_contents=FOO_CONTENTS, status='A'):
42+
assert os.path.exists(path.foo_filename)
43+
assert open(path.foo_filename).read() == foo_contents
44+
actual_status = get_short_git_status()['foo']
45+
assert status == actual_status
46+
47+
48+
def test_foo_staged(foo_staged):
49+
_test_foo_state(foo_staged)
50+
51+
52+
def test_foo_nothing_unstaged(foo_staged, cmd_runner):
53+
with staged_files_only(cmd_runner):
54+
_test_foo_state(foo_staged)
55+
_test_foo_state(foo_staged)
56+
57+
58+
def test_foo_something_unstaged(foo_staged, cmd_runner):
59+
with open(foo_staged.foo_filename, 'w') as foo_file:
60+
foo_file.write('herp\nderp\n')
61+
62+
_test_foo_state(foo_staged, 'herp\nderp\n', 'AM')
63+
64+
with staged_files_only(cmd_runner):
65+
_test_foo_state(foo_staged)
66+
67+
_test_foo_state(foo_staged, 'herp\nderp\n', 'AM')
68+
69+
70+
def test_foo_both_modify_non_conflicting(foo_staged, cmd_runner):
71+
with open(foo_staged.foo_filename, 'w') as foo_file:
72+
foo_file.write(FOO_CONTENTS + '9\n')
73+
74+
_test_foo_state(foo_staged, FOO_CONTENTS + '9\n', 'AM')
75+
76+
with staged_files_only(cmd_runner):
77+
_test_foo_state(foo_staged)
78+
79+
# Modify the file as part of the "pre-commit"
80+
with open(foo_staged.foo_filename, 'w') as foo_file:
81+
foo_file.write(FOO_CONTENTS.replace('1', 'a'))
82+
83+
_test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM')
84+
85+
_test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a') + '9\n', 'AM')
86+
87+
88+
def test_foo_both_modify_conflicting(foo_staged, cmd_runner):
89+
with open(foo_staged.foo_filename, 'w') as foo_file:
90+
foo_file.write(FOO_CONTENTS.replace('1', 'a'))
91+
92+
_test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM')
93+
94+
with staged_files_only(cmd_runner):
95+
_test_foo_state(foo_staged)
96+
97+
# Modify in the same place as the stashed diff
98+
with open(foo_staged.foo_filename, 'w') as foo_file:
99+
foo_file.write(FOO_CONTENTS.replace('1', 'b'))
100+
101+
_test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'b'), 'AM')
102+
103+
_test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM')
104+
105+
106+
@pytest.yield_fixture
107+
def img_staged(empty_git_dir):
108+
with open('.gitignore', 'w') as gitignore_file:
109+
gitignore_file.write(C.HOOKS_WORKSPACE + '\n')
110+
local['git']['add', '.']()
111+
local['git']['commit', '-m', 'add gitignore']()
112+
113+
img_filename = os.path.join(empty_git_dir, 'img.jpg')
114+
shutil.copy(get_resource_path('img1.jpg'), img_filename)
115+
local['git']['add', 'img.jpg']()
116+
yield auto_namedtuple(path=empty_git_dir, img_filename=img_filename)
117+
118+
119+
def _test_img_state(path, expected_file='img1.jpg', status='A'):
120+
assert os.path.exists(path.img_filename)
121+
assert (
122+
open(path.img_filename).read() ==
123+
open(get_resource_path(expected_file)).read()
124+
)
125+
actual_status = get_short_git_status()['img.jpg']
126+
assert status == actual_status
127+
128+
129+
def test_img_staged(img_staged):
130+
_test_img_state(img_staged)
131+
132+
133+
def test_img_nothing_unstaged(img_staged, cmd_runner):
134+
with staged_files_only(cmd_runner):
135+
_test_img_state(img_staged)
136+
_test_img_state(img_staged)
137+
138+
139+
def test_img_something_unstaged(img_staged, cmd_runner):
140+
shutil.copy(get_resource_path('img2.jpg'), img_staged.img_filename)
141+
142+
_test_img_state(img_staged, 'img2.jpg', 'AM')
143+
144+
with staged_files_only(cmd_runner):
145+
_test_img_state(img_staged)
146+
147+
_test_img_state(img_staged, 'img2.jpg', 'AM')
148+
149+
150+
def test_img_conflict(img_staged, cmd_runner):
151+
"""Admittedly, this shouldn't happen, but just in case."""
152+
shutil.copy(get_resource_path('img2.jpg'), img_staged.img_filename)
153+
154+
_test_img_state(img_staged, 'img2.jpg', 'AM')
155+
156+
with staged_files_only(cmd_runner):
157+
_test_img_state(img_staged)
158+
shutil.copy(get_resource_path('img3.jpg'), img_staged.img_filename)
159+
_test_img_state(img_staged, 'img3.jpg', 'AM')
160+
161+
_test_img_state(img_staged, 'img2.jpg', 'AM')
162+
163+
164+
@pytest.yield_fixture
165+
def submodule_with_commits(empty_git_dir):
166+
local['git']['commit', '--allow-empty', '-m', 'foo']()
167+
sha1 = local['git']['rev-parse', 'HEAD']().strip()
168+
local['git']['commit', '--allow-empty', '-m', 'bar']()
169+
sha2 = local['git']['rev-parse', 'HEAD']().strip()
170+
yield auto_namedtuple(path=empty_git_dir, sha1=sha1, sha2=sha2)
171+
172+
173+
def checkout_submodule(sha):
174+
with local.cwd('sub'):
175+
local['git']['checkout', sha]()
176+
177+
178+
@pytest.yield_fixture
179+
def sub_staged(submodule_with_commits, empty_git_dir):
180+
local['git']['submodule', 'add', submodule_with_commits.path, 'sub']()
181+
checkout_submodule(submodule_with_commits.sha1)
182+
local['git']['add', 'sub']()
183+
yield auto_namedtuple(
184+
path=empty_git_dir,
185+
sub_path=os.path.join(empty_git_dir, 'sub'),
186+
submodule=submodule_with_commits,
187+
)
188+
189+
190+
def _test_sub_state(path, sha='sha1', status='A'):
191+
assert os.path.exists(path.sub_path)
192+
with local.cwd(path.sub_path):
193+
actual_sha = local['git']['rev-parse', 'HEAD']().strip()
194+
assert actual_sha == getattr(path.submodule, sha)
195+
actual_status = get_short_git_status()['sub']
196+
assert actual_status == status
197+
198+
199+
def test_sub_staged(sub_staged):
200+
_test_sub_state(sub_staged)
201+
202+
203+
def test_sub_nothing_unstaged(sub_staged, cmd_runner):
204+
with staged_files_only(cmd_runner):
205+
_test_sub_state(sub_staged)
206+
_test_sub_state(sub_staged)
207+
208+
209+
def test_sub_something_unstaged(sub_staged, cmd_runner):
210+
checkout_submodule(sub_staged.submodule.sha2)
211+
212+
_test_sub_state(sub_staged, 'sha2', 'AM')
213+
214+
with staged_files_only(cmd_runner):
215+
# This is different from others, we don't want to touch subs
216+
_test_sub_state(sub_staged, 'sha2', 'AM')
217+
218+
_test_sub_state(sub_staged, 'sha2', 'AM')

0 commit comments

Comments
 (0)