Skip to content

Commit 2aaaddb

Browse files
committed
Fail gracefully on undecodable install output.
1 parent 75aaadd commit 2aaaddb

File tree

9 files changed

+105
-25
lines changed

9 files changed

+105
-25
lines changed

pre_commit/error_handler.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,19 @@ class PreCommitSystemExit(SystemExit):
1818
pass
1919

2020

21+
def _to_bytes(exc):
22+
try:
23+
return bytes(exc)
24+
except Exception:
25+
return five.text(exc).encode('UTF-8')
26+
27+
2128
def _log_and_exit(msg, exc, formatted, write_fn=sys_stdout_write_wrapper):
22-
error_msg = '{0}: {1}: {2}\n'.format(msg, type(exc).__name__, exc)
29+
error_msg = b''.join((
30+
five.to_bytes(msg), b': ',
31+
five.to_bytes(type(exc).__name__), b': ',
32+
_to_bytes(exc), b'\n',
33+
))
2334
write_fn(error_msg)
2435
write_fn('Check the log at ~/.pre-commit/pre-commit.log\n')
2536
store = Store()

pre_commit/languages/node.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,14 @@ def install_environment(
4545
repo_cmd_runner.run(cmd)
4646

4747
with in_env(repo_cmd_runner, version) as node_env:
48-
node_env.run("cd '{prefix}' && npm install -g")
48+
node_env.run("cd '{prefix}' && npm install -g", encoding=None)
4949
if additional_dependencies:
5050
node_env.run(
5151
"cd '{prefix}' && npm install -g " +
5252
' '.join(
5353
shell_escape(dep) for dep in additional_dependencies
54-
)
54+
),
55+
encoding=None,
5556
)
5657

5758

pre_commit/languages/python.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,14 @@ def install_environment(
6363
venv_cmd.extend(['-p', norm_version(version)])
6464
repo_cmd_runner.run(venv_cmd)
6565
with in_env(repo_cmd_runner, version) as env:
66-
env.run("cd '{prefix}' && pip install .")
66+
env.run("cd '{prefix}' && pip install .", encoding=None)
6767
if additional_dependencies:
6868
env.run(
6969
"cd '{prefix}' && pip install " +
7070
' '.join(
7171
shell_escape(dep) for dep in additional_dependencies
72-
)
72+
),
73+
encoding=None,
7374
)
7475

7576

pre_commit/languages/ruby.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,15 @@ def install_environment(
9595
ruby_env.run(
9696
'cd {prefix} && gem build *.gemspec && '
9797
'gem install --no-ri --no-rdoc *.gem',
98+
encoding=None,
9899
)
99100
if additional_dependencies:
100101
ruby_env.run(
101102
'cd {prefix} && gem install --no-ri --no-rdoc ' +
102103
' '.join(
103104
shell_escape(dep) for dep in additional_dependencies
104-
)
105+
),
106+
encoding=None,
105107
)
106108

107109

pre_commit/util.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -124,26 +124,38 @@ def __init__(self, returncode, cmd, expected_returncode, output=None):
124124
self.expected_returncode = expected_returncode
125125
self.output = output
126126

127-
def __str__(self):
127+
def to_bytes(self):
128128
output = []
129-
for text in self.output:
130-
if text:
131-
output.append('\n ' + text.replace('\n', '\n '))
129+
for maybe_text in self.output:
130+
if maybe_text:
131+
output.append(
132+
b'\n ' +
133+
five.to_bytes(maybe_text).replace(b'\n', b'\n ')
134+
)
132135
else:
133-
output.append('(none)')
134-
135-
return (
136-
'Command: {0!r}\n'
137-
'Return code: {1}\n'
138-
'Expected return code: {2}\n'
139-
'Output: {3}\n'
140-
'Errors: {4}\n'.format(
141-
self.cmd,
142-
self.returncode,
143-
self.expected_returncode,
144-
*output
145-
)
146-
)
136+
output.append(b'(none)')
137+
138+
return b''.join((
139+
five.to_bytes(
140+
'Command: {0!r}\n'
141+
'Return code: {1}\n'
142+
'Expected return code: {2}\n'.format(
143+
self.cmd, self.returncode, self.expected_returncode
144+
)
145+
),
146+
b'Output: ', output[0], b'\n',
147+
b'Errors: ', output[1], b'\n',
148+
))
149+
150+
def to_text(self):
151+
return self.to_bytes().decode('UTF-8')
152+
153+
if five.PY3: # pragma: no cover
154+
__bytes__ = to_bytes
155+
__str__ = to_text
156+
else:
157+
__str__ = to_bytes
158+
__unicode__ = to_text
147159

148160

149161
def cmd_output(*cmd, **kwargs):
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
- id: foo
2+
name: Foo
3+
entry: foo
4+
language: python
5+
language_version: python2.7
6+
files: \.py$
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import print_function
3+
from __future__ import unicode_literals
4+
5+
import sys
6+
7+
8+
def main():
9+
# Intentionally write mixed encoding to the output. This should not crash
10+
# pre-commit and should write bytes to the output.
11+
sys.stderr.write('☃'.encode('UTF-8') + '²'.encode('latin1') + b'\n')
12+
# Return 1 to indicate failures
13+
return 1
14+
15+
16+
if __name__ == '__main__':
17+
exit(main())

tests/commands/run_test.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,33 @@ def test_stdout_write_bug_py26(
371371
assert 'UnicodeDecodeError' not in stdout
372372

373373

374+
def test_hook_install_failure(mock_out_store_directory, tempdir_factory):
375+
git_path = make_consuming_repo(tempdir_factory, 'not_installable_repo')
376+
with cwd(git_path):
377+
install(Runner(git_path))
378+
379+
# Don't want to write to home directory
380+
env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get())
381+
_, stdout, _ = cmd_output(
382+
'git', 'commit', '-m', 'Commit!',
383+
# git commit puts pre-commit to stderr
384+
stderr=subprocess.STDOUT,
385+
env=env,
386+
retcode=None,
387+
encoding=None,
388+
)
389+
assert b'UnicodeDecodeError' not in stdout
390+
# Doesn't actually happen, but a reasonable assertion
391+
assert b'UnicodeEncodeError' not in stdout
392+
393+
# Sanity check our output
394+
assert (
395+
b'An unexpected error has occurred: CalledProcessError: ' in
396+
stdout
397+
)
398+
assert '☃'.encode('UTF-8') + '²'.encode('latin1') in stdout
399+
400+
374401
def test_get_changed_files():
375402
files = get_changed_files(
376403
'78c682a1d13ba20e7cb735313b9314a74365cd3a',

tests/error_handler_test.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import pytest
1212

1313
from pre_commit import error_handler
14+
from pre_commit import five
1415
from pre_commit.errors import FatalError
1516
from pre_commit.util import cmd_output
1617

@@ -82,7 +83,9 @@ def test_log_and_exit(mock_out_store_directory):
8283
write_fn=mocked_write,
8384
)
8485

85-
printed = ''.join(call[0][0] for call in mocked_write.call_args_list)
86+
printed = ''.join(
87+
five.to_text(call[0][0]) for call in mocked_write.call_args_list
88+
)
8689
assert printed == (
8790
'msg: FatalError: hai\n'
8891
'Check the log at ~/.pre-commit/pre-commit.log\n'

0 commit comments

Comments
 (0)