Skip to content

Commit f3516cf

Browse files
Stash unstaged changes before running pylint
1 parent 0b967a4 commit f3516cf

File tree

2 files changed

+225
-81
lines changed

2 files changed

+225
-81
lines changed

git_pylint_commit_hook/commit_hook.py

Lines changed: 129 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import subprocess
77
import collections
88
import ConfigParser
9+
import contextlib
910

1011

1112
ExecutionResult = collections.namedtuple(
@@ -32,6 +33,15 @@ def _current_commit():
3233
return 'HEAD'
3334

3435

36+
def _current_stash():
37+
res = _execute('git rev-parse -q --verify refs/stash'.split())
38+
if res.status:
39+
# not really as meaningful for a stash, but makes some sense
40+
return '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
41+
else:
42+
return res.stdout
43+
44+
3545
def _get_list_of_committed_files():
3646
""" Returns a list of files about to be commited. """
3747
files = []
@@ -81,6 +91,41 @@ def _parse_score(pylint_output):
8191
return 0.0
8292

8393

94+
@contextlib.contextmanager
95+
def _stash_unstaged():
96+
"""Stashes any changes on entry and restores them on exit.
97+
98+
If there is no initial commit, print a warning and do nothing.
99+
100+
"""
101+
if _current_commit() != 'HEAD':
102+
# git stash doesn't work with no initial commit, so warn and do nothing
103+
print('WARNING: unable to stash changes with no initial commit')
104+
yield
105+
return
106+
107+
# git stash still returns 0 if there is nothing to stash,
108+
# so inspect the current stash before and after saving
109+
original_stash = _current_stash()
110+
# leave a message marking the stash as ours in case something goes wrong
111+
# so that the user can work out what happened and fix things manually
112+
subprocess.check_call('git stash save -q --keep-index '
113+
'git-pylint-commit-hook'.split())
114+
new_stash = _current_stash()
115+
116+
try:
117+
# let the caller do whatever they wanted to do
118+
# (but we still want to restore the tree if an exception was thrown)
119+
yield
120+
finally:
121+
# only restore if we actually stashed something
122+
if original_stash != new_stash:
123+
# avoid merge issues
124+
subprocess.check_call('git reset --hard -q'.split())
125+
# restore everything to how it was
126+
subprocess.check_call('git stash pop --index -q'.split())
127+
128+
84129
def check_repo(
85130
limit, pylint='pylint', pylintrc='.pylintrc', pylint_params=None,
86131
suppress_report=False):
@@ -103,92 +148,95 @@ def check_repo(
103148
# Set the exit code
104149
all_filed_passed = True
105150

106-
# Find Python files
107-
for filename in _get_list_of_committed_files():
108-
try:
109-
if _is_python_file(filename):
110-
python_files.append((filename, None))
111-
except IOError:
112-
print 'File not found (probably deleted): {}\t\tSKIPPED'.format(
113-
filename)
114-
115-
# Don't do anything if there are no Python files
116-
if len(python_files) == 0:
117-
sys.exit(0)
118-
119-
# Load any pre-commit-hooks options from a .pylintrc file (if there is one)
120-
if os.path.exists(pylintrc):
121-
conf = ConfigParser.SafeConfigParser()
122-
conf.read(pylintrc)
123-
if conf.has_option('pre-commit-hook', 'command'):
124-
pylint = conf.get('pre-commit-hook', 'command')
125-
if conf.has_option('pre-commit-hook', 'params'):
126-
pylint_params += ' ' + conf.get('pre-commit-hook', 'params')
127-
if conf.has_option('pre-commit-hook', 'limit'):
128-
limit = float(conf.get('pre-commit-hook', 'limit'))
129-
130-
# Pylint Python files
131-
i = 1
132-
for python_file, score in python_files:
133-
# Allow __init__.py files to be completely empty
134-
if os.path.basename(python_file) == '__init__.py':
135-
if os.stat(python_file).st_size == 0:
136-
print(
137-
'Skipping pylint on {} (empty __init__.py)..'
138-
'\tSKIPPED'.format(python_file))
139-
140-
# Bump parsed files
141-
i += 1
142-
continue
143-
144-
# Start pylinting
145-
sys.stdout.write("Running pylint on {} (file {}/{})..\t".format(
146-
python_file, i, len(python_files)))
147-
sys.stdout.flush()
148-
try:
149-
command = [pylint]
150-
151-
if pylint_params:
152-
command += pylint_params.split()
153-
if '--rcfile' not in pylint_params:
151+
# Stash any unstaged changes while we look at the tree
152+
with _stash_unstaged():
153+
# Find Python files
154+
for filename in _get_list_of_committed_files():
155+
try:
156+
if _is_python_file(filename):
157+
python_files.append((filename, None))
158+
except IOError:
159+
print 'File not found (probably deleted): {}\t\tSKIPPED'.format(
160+
filename)
161+
162+
# Don't do anything if there are no Python files
163+
if len(python_files) == 0:
164+
sys.exit(0)
165+
166+
# Load any pre-commit-hooks options from a .pylintrc file
167+
# (if there is one)
168+
if os.path.exists(pylintrc):
169+
conf = ConfigParser.SafeConfigParser()
170+
conf.read(pylintrc)
171+
if conf.has_option('pre-commit-hook', 'command'):
172+
pylint = conf.get('pre-commit-hook', 'command')
173+
if conf.has_option('pre-commit-hook', 'params'):
174+
pylint_params += ' ' + conf.get('pre-commit-hook', 'params')
175+
if conf.has_option('pre-commit-hook', 'limit'):
176+
limit = float(conf.get('pre-commit-hook', 'limit'))
177+
178+
# Pylint Python files
179+
i = 1
180+
for python_file, score in python_files:
181+
# Allow __init__.py files to be completely empty
182+
if os.path.basename(python_file) == '__init__.py':
183+
if os.stat(python_file).st_size == 0:
184+
print(
185+
'Skipping pylint on {} (empty __init__.py)..'
186+
'\tSKIPPED'.format(python_file))
187+
188+
# Bump parsed files
189+
i += 1
190+
continue
191+
192+
# Start pylinting
193+
sys.stdout.write("Running pylint on {} (file {}/{})..\t".format(
194+
python_file, i, len(python_files)))
195+
sys.stdout.flush()
196+
try:
197+
command = [pylint]
198+
199+
if pylint_params:
200+
command += pylint_params.split()
201+
if '--rcfile' not in pylint_params:
202+
command.append('--rcfile={}'.format(pylintrc))
203+
else:
154204
command.append('--rcfile={}'.format(pylintrc))
155-
else:
156-
command.append('--rcfile={}'.format(pylintrc))
157-
158-
159-
command.append(python_file)
160-
161-
proc = subprocess.Popen(
162-
command,
163-
stdout=subprocess.PIPE,
164-
stderr=subprocess.PIPE)
165-
out, _ = proc.communicate()
166-
except OSError:
167-
print("\nAn error occurred. Is pylint installed?")
168-
sys.exit(1)
169-
170-
# Verify the score
171-
score = _parse_score(out)
172-
if score >= float(limit):
173-
status = 'PASSED'
174-
else:
175-
status = 'FAILED'
176-
all_filed_passed = False
177-
178-
# Add some output
179-
print('{:.2}/10.00\t{}'.format(decimal.Decimal(score), status))
180-
if 'FAILED' in status:
181-
if suppress_report:
182-
command.append('--reports=n')
205+
206+
207+
command.append(python_file)
208+
183209
proc = subprocess.Popen(
184210
command,
185211
stdout=subprocess.PIPE,
186212
stderr=subprocess.PIPE)
187213
out, _ = proc.communicate()
188-
print out
189-
190-
191-
# Bump parsed files
192-
i += 1
214+
except OSError:
215+
print("\nAn error occurred. Is pylint installed?")
216+
sys.exit(1)
217+
218+
# Verify the score
219+
score = _parse_score(out)
220+
if score >= float(limit):
221+
status = 'PASSED'
222+
else:
223+
status = 'FAILED'
224+
all_filed_passed = False
225+
226+
# Add some output
227+
print('{:.2}/10.00\t{}'.format(decimal.Decimal(score), status))
228+
if 'FAILED' in status:
229+
if suppress_report:
230+
command.append('--reports=n')
231+
proc = subprocess.Popen(
232+
command,
233+
stdout=subprocess.PIPE,
234+
stderr=subprocess.PIPE)
235+
out, _ = proc.communicate()
236+
print out
237+
238+
239+
# Bump parsed files
240+
i += 1
193241

194242
return all_filed_passed

tests.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88

99
from git_pylint_commit_hook import commit_hook
1010

11+
12+
class TestException(Exception):
13+
pass
14+
15+
1116
class TestHook(unittest.TestCase):
1217
# pylint: disable=protected-access,too-many-public-methods,invalid-name
1318

@@ -44,6 +49,27 @@ def test_current_commit(self):
4449
self.cmd('git commit --allow-empty -m msg')
4550
self.assertEquals(commit_hook._current_commit(), 'HEAD')
4651

52+
def test_current_stash(self):
53+
"""Test commit_hook._current_stash"""
54+
55+
# Test empty stash
56+
empty_hash = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
57+
self.assertEquals(commit_hook._current_stash(), empty_hash)
58+
59+
# Need an initial commit to stash
60+
self.cmd('git commit --allow-empty -m msg')
61+
62+
# Test after stash
63+
self.write_file('a', 'foo')
64+
self.cmd('git stash --include-untracked')
65+
stash = commit_hook._current_stash()
66+
self.assertNotEquals(stash, empty_hash)
67+
68+
# Test the next stash doesn't look like the last
69+
self.write_file('b', 'bar')
70+
self.cmd('git stash --include-untracked')
71+
self.assertNotEquals(commit_hook._current_stash(), stash)
72+
4773
def test_list_of_committed_files(self):
4874
"""Test commit_hook._get_list_of_committed_files"""
4975

@@ -93,3 +119,73 @@ def test_parse_score(self):
93119

94120
text = 'Your code has been rated at 8.51'
95121
self.assertEquals(commit_hook._parse_score(text), 0.0)
122+
123+
def test_stash_unstaged(self):
124+
"""Test commit_hook._stash_unstaged"""
125+
126+
# git stash doesn't work without an initial commit :(
127+
self.cmd('git commit --allow-empty -m msg')
128+
129+
# Write a file with a style error
130+
a = self.write_file('a', 'style error!')
131+
# Add it to the repository
132+
self.cmd('git add ' + a)
133+
134+
# Fix the style error but don't add it
135+
self.write_file('a', 'much better :)')
136+
137+
# Stash any unstaged changes; check the unstaged changes disappear
138+
with commit_hook._stash_unstaged():
139+
with open(a) as f:
140+
self.assertEquals(f.read(), 'style error!')
141+
142+
# Check the unstaged changes return
143+
with open(a) as f:
144+
self.assertEquals(f.read(), 'much better :)')
145+
146+
# Stash changes then pretend we crashed
147+
with self.assertRaises(TestException):
148+
with commit_hook._stash_unstaged():
149+
raise TestException
150+
151+
# Check the unstaged changes return
152+
with open(a) as f:
153+
self.assertEquals(f.read(), 'much better :)')
154+
155+
def test_stash_unstaged_untracked(self):
156+
"""Test commit_hook._stash_unstaged leaves untracked files alone"""
157+
158+
# git stash doesn't work without an initial commit :(
159+
self.cmd('git commit --allow-empty -m msg')
160+
161+
# Write a file but don't add it
162+
a = self.write_file('a', 'untracked')
163+
164+
# Stash changes; check nothing happened
165+
with commit_hook._stash_unstaged():
166+
with open(a) as f:
167+
self.assertEqual(f.read(), 'untracked')
168+
169+
# Check the file is still unmodified
170+
with open(a) as f:
171+
self.assertEquals(f.read(), 'untracked')
172+
173+
def test_stash_unstaged_no_initial(self):
174+
"""Test commit_hook._stash_unstaged handles no initial commit"""
175+
176+
# Write a file with a style error
177+
a = self.write_file('a', 'style error!')
178+
# Add it to the repository
179+
self.cmd('git add ' + a)
180+
181+
# Fix the style error but don't add it
182+
self.write_file('a', 'much better :)')
183+
184+
# A warning should be printed to screen saying nothing is stashed
185+
with commit_hook._stash_unstaged():
186+
with open(a) as f:
187+
self.assertEquals(f.read(), 'much better :)')
188+
189+
# Check the file is still unmodified
190+
with open(a) as f:
191+
self.assertEquals(f.read(), 'much better :)')

0 commit comments

Comments
 (0)