Skip to content

Commit

Permalink
feat: check for required git hooks (#854)
Browse files Browse the repository at this point in the history
* feat: renku doctor checks for git hooks and their content
  • Loading branch information
m-alisafaee committed Dec 12, 2019
1 parent 0b60ec8 commit 54ba91d
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 24 deletions.
2 changes: 2 additions & 0 deletions renku/core/commands/checks/__init__.py
Expand Up @@ -17,13 +17,15 @@
# limitations under the License.
"""Define repository checks for :program:`renku doctor`."""

from .githooks import check_git_hooks_installed
from .migration import check_dataset_metadata, check_missing_files
from .references import check_missing_references
from .validate_shacl import check_project_structure, check_datasets_structure

# Checks will be executed in the order as they are listed in __all__.
# They are mostly used in ``doctor`` command to inspect broken things.
__all__ = (
'check_git_hooks_installed',
'check_dataset_metadata',
'check_missing_files',
'check_missing_references',
Expand Down
72 changes: 72 additions & 0 deletions renku/core/commands/checks/githooks.py
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
#
# Copyright 2019 - Swiss Data Science Center (SDSC)
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
# Eidgenössische Technische Hochschule Zürich (ETHZ).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Check for required Git hooks."""
from io import StringIO
from pathlib import Path

import pkg_resources
from git.index.fun import hook_path as get_hook_path

from renku.core.management.githooks import HOOKS

from ..echo import WARNING


def check_git_hooks_installed(client):
"""Checks if all necessary hooks are installed."""
for hook in HOOKS:
hook_path = Path(get_hook_path(hook, client.repo.git_dir))
if not hook_path.exists():
message = WARNING + 'Git hooks are not installed. ' \
'Use "renku githooks install" to install them. \n'
return False, message

with hook_path.open() as file_:
actual_hook = _extract_renku_hook(file_)
with StringIO(_read_resource(hook)) as file_:
expected_hook = _extract_renku_hook(file_)

if not expected_hook:
message = WARNING + 'Cannot check for existence of Git hooks.\n'
return False, message

if actual_hook != expected_hook:
message = WARNING + 'Git hooks are outdated or not installed. ' \
'Use "renku githooks install --force" to update them. \n'
return False, message

return True, None


def _extract_renku_hook(file_):
lines = [line.strip() for line in file_ if line.strip()]
start = end = -1
for index, line in enumerate(lines):
if line.startswith('# RENKU HOOK.'):
start = index
elif line.endswith('# END RENKU HOOK.'):
end = index
break

return lines[start:end] if 0 <= start <= end else []


def _read_resource(hook):
return pkg_resources.resource_string(
'renku.data', '{hook}.sh'.format(hook=hook)
).decode('utf-8')
52 changes: 28 additions & 24 deletions renku/data/pre-commit.sh
Expand Up @@ -17,31 +17,35 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# Find all modified files, and exit early if there aren't any.
######################################
# RENKU HOOK. DO NOT REMOVE OR MODIFY.
######################################

# Find all modified files, and do nothing if there aren't any.
MODIFIED_FILES=$(git diff --name-only --cached --diff-filter=M)
if [ ! "$MODIFIED_FILES" ]; then
exit 0
fi
if [ "$MODIFIED_FILES" ]; then
# Verify that renku is installed; if not, warn and exit.
if [ -z "$(command -v renku)" ]; then
echo 'renku not on path; can not format. Please install renku:'
# TODO add system detection and recommend brew for macOS.
echo ' pip install renku'
exit 2
fi

# Verify that renku is installed; if not, warn and exit.
if [ -z "$(command -v renku)" ]; then
echo 'renku not on path; can not format. Please install renku:'
# TODO add system detection and recommend brew for macOS.
echo ' pip install renku'
exit 2
MODIFIED_OUTPUTS=$(renku show outputs "${MODIFIED_FILES[@]}")
if [ "$MODIFIED_OUTPUTS" ]; then
echo 'You are trying to update generated files.'
echo
echo 'Modified files:'
for file in "${MODIFIED_OUTPUTS[@]}"; do
echo " $file"
done
echo
echo 'To commit anyway, use "git commit --no-verify".'
exit 1
fi
fi

MODIFIED_OUTPUTS=$(renku show outputs "${MODIFIED_FILES[@]}")
if [ "$MODIFIED_OUTPUTS" ]; then
echo 'You are trying to update generated files.'
echo
echo 'Modified files:'
for file in "${MODIFIED_OUTPUTS[@]}"; do
echo " $file"
done
echo
echo 'To commit anyway, use "git commit --no-verify".'
exit 1
else
exit 0
fi
######################################
# END RENKU HOOK.
######################################
61 changes: 61 additions & 0 deletions tests/core/commands/test_doctor.py
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
#
# Copyright 2017-2019 - Swiss Data Science Center (SDSC)
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
# Eidgenössische Technische Hochschule Zürich (ETHZ).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Renku doctor tests."""
from pathlib import Path

from renku.cli import cli


def test_git_hooks(runner, project):
"""Test detection of not-installed git hooks."""
# Initially, every thing is OK
result = runner.invoke(cli, ['doctor'])
assert 0 == result.exit_code
assert 'Everything seems to be ok.' in result.output

result = runner.invoke(cli, ['githooks', 'uninstall'])
assert 0 == result.exit_code

result = runner.invoke(cli, ['doctor'])
assert 1 == result.exit_code
assert 'Git hooks are not installed.' in result.output


def test_git_hooks_modified(runner, project):
"""Test detection of modified git hooks."""
result = runner.invoke(cli, ['githooks', 'install', '--force'])
assert 0 == result.exit_code

hook_path = Path(project) / '.git' / 'hooks' / 'pre-commit'
lines = hook_path.read_text().split('/n')

# Append some more commands
appended = lines + ['# Some more commands', 'ls']
hook_path.write_text('\n'.join(appended))

# Check passes as long as Renku hook is not modified
result = runner.invoke(cli, ['doctor'])
assert 0 == result.exit_code
assert 'Everything seems to be ok.' in result.output

# Modify Renku hook
hook_path.write_text('\n'.join(lines[:-5]))

result = runner.invoke(cli, ['doctor'])
assert 1 == result.exit_code
assert 'Git hooks are outdated or not installed.' in result.output

0 comments on commit 54ba91d

Please sign in to comment.