Making it possible to invoke pre-commit run --files some.file from a subdirectory of the repository

This commit is contained in:
Lucas Cimon 2016-02-03 09:03:59 +01:00
parent 894862462d
commit df8102f57d
4 changed files with 72 additions and 29 deletions

View file

@ -8,6 +8,7 @@ import pkg_resources
from pre_commit import color from pre_commit import color
from pre_commit import five from pre_commit import five
from pre_commit import git
from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import autoupdate
from pre_commit.commands.clean import clean from pre_commit.commands.clean import clean
from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install
@ -110,7 +111,8 @@ def main(argv=None):
help='Run on all the files in the repo. Implies --no-stash.', help='Run on all the files in the repo. Implies --no-stash.',
) )
run_mutex_group.add_argument( run_mutex_group.add_argument(
'--files', nargs='*', help='Specific filenames to run hooks on.', '--files', nargs='*', default=[],
help='Specific filenames to run hooks on.',
) )
help = subparsers.add_parser( help = subparsers.add_parser(
@ -122,6 +124,11 @@ def main(argv=None):
if len(argv) == 0: if len(argv) == 0:
argv = ['run'] argv = ['run']
args = parser.parse_args(argv) args = parser.parse_args(argv)
if args.command == 'run':
args.files = [
os.path.relpath(os.path.abspath(filename), git.get_root())
for filename in args.files
]
if args.command == 'help': if args.command == 'help':
if args.help_cmd: if args.help_cmd:

View file

@ -24,7 +24,7 @@ class Runner(object):
def create(cls): def create(cls):
"""Creates a PreCommitRunner by doing the following: """Creates a PreCommitRunner by doing the following:
- Finds the root of the current git repository - Finds the root of the current git repository
- chdirs to that directory - chdir to that directory
""" """
root = git.get_root() root = git.get_root()
os.chdir(root) os.chdir(root)

View file

@ -110,8 +110,14 @@ def add_config_to_repo(git_path, config):
return git_path return git_path
def make_consuming_repo(tempdir_factory, repo_source): def make_consuming_repo(tempdir_factory, repo_source, local_hooks=None):
path = make_repo(tempdir_factory, repo_source) path = make_repo(tempdir_factory, repo_source)
config = make_config_from_repo(path) if local_hooks:
config = OrderedDict((
('repo', 'local'),
('hooks', local_hooks),
))
else:
config = make_config_from_repo(path)
git_path = git_dir(tempdir_factory) git_path = git_dir(tempdir_factory)
return add_config_to_repo(git_path, config) return add_config_to_repo(git_path, config)

View file

@ -42,7 +42,7 @@ def repo_with_failing_hook(tempdir_factory):
yield git_path yield git_path
def stage_a_file(filename='foo.py'): def stage_a_file(filename):
cmd_output('touch', filename) cmd_output('touch', filename)
cmd_output('git', 'add', filename) cmd_output('git', 'add', filename)
@ -83,14 +83,15 @@ def _do_run(repo, args, environ={}):
runner = Runner(repo) runner = Runner(repo)
write_mock = mock.Mock() write_mock = mock.Mock()
write_fn = functools.partial(sys_stdout_write_wrapper, stream=write_mock) write_fn = functools.partial(sys_stdout_write_wrapper, stream=write_mock)
ret = run(runner, args, write=write_fn, environ=environ) with cwd(runner.git_root): # replicates Runner.create behaviour
ret = run(runner, args, write=write_fn, environ=environ)
printed = get_write_mock_output(write_mock) printed = get_write_mock_output(write_mock)
return ret, printed return ret, printed
def _test_run(repo, options, expected_outputs, expected_ret, stage): def _test_run(repo, options, expected_outputs, expected_ret, file_to_stage):
if stage: if file_to_stage:
stage_a_file() stage_a_file(file_to_stage)
args = _get_opts(**options) args = _get_opts(**options)
ret, printed = _do_run(repo, args) ret, printed = _do_run(repo, args)
@ -112,14 +113,14 @@ def test_run_all_hooks_failing(
b'Fail\nfoo.py\n', b'Fail\nfoo.py\n',
), ),
1, 1,
True, 'foo.py',
) )
def test_arbitrary_bytes_hook(tempdir_factory, mock_out_store_directory): def test_arbitrary_bytes_hook(tempdir_factory, mock_out_store_directory):
git_path = make_consuming_repo(tempdir_factory, 'arbitrary_bytes_repo') git_path = make_consuming_repo(tempdir_factory, 'arbitrary_bytes_repo')
with cwd(git_path): with cwd(git_path):
_test_run(git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True) _test_run(git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, 'foo.py')
def test_hook_that_modifies_but_returns_zero( def test_hook_that_modifies_but_returns_zero(
@ -147,30 +148,30 @@ def test_hook_that_modifies_but_returns_zero(
b'Files were modified by this hook.\n', b'Files were modified by this hook.\n',
), ),
1, 1,
True, 'foo.py',
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
('options', 'outputs', 'expected_ret', 'stage'), ('options', 'outputs', 'expected_ret', 'file_to_stage'),
( (
({}, (b'Bash hook', b'Passed'), 0, True), ({}, (b'Bash hook', b'Passed'), 0, 'foo.py'),
({'verbose': True}, (b'foo.py\nHello World',), 0, True), ({'verbose': True}, (b'foo.py\nHello World',), 0, 'foo.py'),
({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, 'foo.py'),
({'hook': 'nope'}, (b'No hook with id `nope`',), 1, True), ({'hook': 'nope'}, (b'No hook with id `nope`',), 1, 'foo.py'),
( (
{'all_files': True, 'verbose': True}, {'all_files': True, 'verbose': True},
(b'foo.py',), (b'foo.py',),
0, 0,
True, 'foo.py',
), ),
( (
{'files': ('foo.py',), 'verbose': True}, {'files': ('foo.py',), 'verbose': True},
(b'foo.py',), (b'foo.py',),
0, 0,
True, 'foo.py',
), ),
({}, (b'Bash hook', b'(no files to check)', b'Skipped'), 0, False), ({}, (b'Bash hook', b'(no files to check)', b'Skipped'), 0, None),
) )
) )
def test_run( def test_run(
@ -178,16 +179,17 @@ def test_run(
options, options,
outputs, outputs,
expected_ret, expected_ret,
stage, file_to_stage,
mock_out_store_directory, mock_out_store_directory,
): ):
_test_run(repo_with_passing_hook, options, outputs, expected_ret, stage) _test_run(repo_with_passing_hook, options, outputs, expected_ret,
file_to_stage)
def test_always_run(repo_with_passing_hook, mock_out_store_directory): def test_always_run(repo_with_passing_hook, mock_out_store_directory):
with modify_config() as config: with modify_config() as config:
config[0]['hooks'][0]['always_run'] = True config[0]['hooks'][0]['always_run'] = True
_test_run(repo_with_passing_hook, {}, (b'Bash hook', b'Passed'), 0, False) _test_run(repo_with_passing_hook, {}, (b'Bash hook', b'Passed'), 0, None)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -229,7 +231,7 @@ def test_no_stash(
expect_stash, expect_stash,
mock_out_store_directory, mock_out_store_directory,
): ):
stage_a_file() stage_a_file('foo.py')
# Make unstaged changes # Make unstaged changes
with open('foo.py', 'w') as foo_file: with open('foo.py', 'w') as foo_file:
foo_file.write('import os\n') foo_file.write('import os\n')
@ -323,7 +325,7 @@ def test_multiple_hooks_same_id(
# Add bash hook on there again # Add bash hook on there again
with modify_config() as config: with modify_config() as config:
config[0]['hooks'].append({'id': 'bash_hook'}) config[0]['hooks'].append({'id': 'bash_hook'})
stage_a_file() stage_a_file('foo.py')
ret, output = _do_run(repo_with_passing_hook, _get_opts()) ret, output = _do_run(repo_with_passing_hook, _get_opts())
assert ret == 0 assert ret == 0
@ -352,7 +354,7 @@ def test_stdout_write_bug_py26(
with cwd(repo_with_failing_hook): with cwd(repo_with_failing_hook):
with modify_config() as config: with modify_config() as config:
config[0]['hooks'][0]['args'] = [''] config[0]['hooks'][0]['args'] = ['']
stage_a_file() stage_a_file('foo.py')
install(Runner(repo_with_failing_hook)) install(Runner(repo_with_failing_hook))
@ -489,7 +491,7 @@ def test_local_hook_for_stages(
{'hook_stage': hook_stage}, {'hook_stage': hook_stage},
expected_outputs=expected_output, expected_outputs=expected_output,
expected_ret=0, expected_ret=0,
stage=False file_to_stage=None
) )
@ -521,7 +523,7 @@ def test_local_hook_passes(repo_with_passing_hook, mock_out_store_directory):
options={}, options={},
expected_outputs=[b''], expected_outputs=[b''],
expected_ret=0, expected_ret=0,
stage=False file_to_stage=None
) )
@ -547,7 +549,7 @@ def test_local_hook_fails(repo_with_passing_hook, mock_out_store_directory):
options={}, options={},
expected_outputs=[b''], expected_outputs=[b''],
expected_ret=1, expected_ret=1,
stage=False, file_to_stage=None
) )
@ -595,3 +597,31 @@ def test_unstaged_message_suppressed(
args = _get_opts(**opts) args = _get_opts(**opts)
ret, printed = _do_run(modified_config_repo, args) ret, printed = _do_run(modified_config_repo, args)
assert b'Your .pre-commit-config.yaml is unstaged.' not in printed assert b'Your .pre-commit-config.yaml is unstaged.' not in printed
def test_invoke_precommit_in_subdir(
tempdir_factory, mock_out_store_directory,
):
subdir_hook = OrderedDict((
('id', 'subdir_bash_hook'),
('name', 'Selective Bash hook'),
('language', 'system'),
('entry', 'echo OK'),
('files', '^subdir/'),
))
git_path = make_consuming_repo(
tempdir_factory,
'script_hooks_repo',
local_hooks=[subdir_hook],
)
with cwd(git_path):
os.mkdir('subdir/')
with cwd('subdir/'):
cmd_output('touch', 'foo.py')
_test_run(
git_path,
options={'files': ('subdir/foo.py',), 'verbose': True},
expected_outputs=(b'foo.py',),
expected_ret=0,
file_to_stage=None,
)