Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new mode autohooks config setting #24

Merged
merged 27 commits into from
Sep 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b456f0d
Add new mode autohooks config setting
bjoernricks Apr 29, 2019
7ac23ed
Consider mode for shebang line at pre-commit hook
bjoernricks May 6, 2019
fdb1858
Move Mode enum into a settings module
bjoernricks May 11, 2019
c00f6e9
Extend Mode enum and add tests
bjoernricks May 11, 2019
75b18cc
Remove extra newline from precommit template
bjoernricks May 11, 2019
12216b7
Move template handling into an own module
bjoernricks May 11, 2019
4e86cbc
Use new PreCommitTemplate class in install
bjoernricks May 11, 2019
1042302
Add new function to verify autohooks precommit hook
bjoernricks May 11, 2019
e26b507
Update check cli to use is_autohooks_pre_commit_hook
bjoernricks May 11, 2019
f60eb33
Make getting the mode from the config more readable
bjoernricks May 11, 2019
928141b
Start to require autohooks
bjoernricks Aug 30, 2019
06d5fcd
Update default template test
bjoernricks Aug 30, 2019
8976eef
Update shebang line for pipenv mode
bjoernricks Aug 30, 2019
638e419
Rename Mode.MANUAL to Mode.PYTHONPATH
bjoernricks Sep 6, 2019
c7e50bd
Add testcase for the PYTHONPATH config mode
bjoernricks Sep 6, 2019
3f57714
Add static from_string method to Mode enum
bjoernricks Sep 6, 2019
f360b78
Override str conversion method for Mode enum
bjoernricks Sep 6, 2019
0815c9b
Set file permission of installed git pre-commit script
bjoernricks Sep 6, 2019
b8c2b82
Update get_autohooks_pre_commit hook function to expect mode
bjoernricks Sep 6, 2019
6adecf8
Print used mode during hook activation
bjoernricks Sep 6, 2019
844fb31
Allow to configure mode during activation
bjoernricks Sep 6, 2019
dab852b
Explain the modes in the README
bjoernricks Sep 6, 2019
4910d96
Update README
bjoernricks Sep 7, 2019
2f4ab59
Add changelog entries for mode and hook changes
bjoernricks Sep 7, 2019
90f5502
Ignore false positive pylint warning
bjoernricks Sep 7, 2019
f2210e3
Apply suggestions from code review
bjoernricks Sep 12, 2019
96cafaf
Add sentence about default fallback mode to README
bjoernricks Sep 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added

* Introduction of autohooks modes. Modes configure how to handle loading
autohooks, plugins and dependencies when running the git hook. The
`pythonpath` mode requires to put the necessary python packages into the
**PYTHONPATH** manually. The `pipenv` mode uses pipenv to handle the
dependencies. Using the `pipenv` mode is recommended.
[#24](https://github.com/greenbone/autohooks/pull/24)

### Changed

* The installed git hook will fail now if autohooks can't be loaded. Before the
git hook raised only a warning and was ignored. This a major change compared
to the previous versions. To update existing installations it requires
overriding the installed git hook by running `autohooks activate --force`.
[#24](https://github.com/greenbone/autohooks/pull/24)

### Deprecated
### Fixed
### Removed
Expand Down
148 changes: 82 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ in Python

- [Why?](#why)
- [Solution](#solution)
- [Modes](#modes)
- [Pythonpath mode](#pythonpath-mode)
- [Pipenv mode](#pipenv-mode)
- [Installation](#installation)
- [Install autohooks python package](#install-autohooks-python-package)
- [Configure mode and plugins to be run](#configure-mode-and-plugins-to-be-run)
- [Activating the git hooks](#activating-the-git-hooks)
- [Configure plugins to be run](#configure-plugins-to-be-run)
- [Proposed Workflow](#proposed-workflow)
- [Plugins](#plugins)
- [How-to write a Plugin](#how-to-write-a-plugin)
- [Linting plugin](#linting-plugin)
Expand All @@ -39,26 +41,65 @@ itself and doesn't install in the current environment.

Autohooks is a pure python library that installs a minimal
[executable git hook](https://github.com/greenbone/autohooks/blob/master/autohooks/precommit/template).
If autohooks isn't installed in your current python path the hooks aren't
executed. So autohooks is always opt-in by installing the package into your
current development environment. It would be even possible to run different
versions of autohooks by switching the environment.
It allows you to decide how to maintain your hook dependencies by supporting
different modes.

Autohooks doesn't interfere with your work. If autohooks can't be run or fails
executing a plugin, an error is shown only and the git hook will proceed.
## Modes

Currently two modes for using autohooks are supported:

* `pythonpath` and
* `pipenv`

The modes handle how autohooks, the plugins and their dependencies are loaded
during git hook execution.

bjoernricks marked this conversation as resolved.
Show resolved Hide resolved
If no mode is specified in the [`pyproject.toml` config file](#configure-mode-and-plugins-to-be-run)
and no mode is set during [activation](#activating-the-git-hooks), autohooks
will use the [pythonpath mode](#pythonpath-mode) by default.

### Pythonpath mode

In the `pythonpath` mode the user has to install autohooks, the desired
plugins and their dependencies into the [PYTHONPATH](https://docs.python.org/3/library/sys.html#sys.path)
manually.

This can be achieved by running `pip install --user autohooks ...` to put them
into the installation directory of the [current user](https://docs.python.org/3/library/site.html#site.USER_SITE)
or with `pip install authooks ...` for a system wide installation.

Alternatively a [virtual environment](https://packaging.python.org/tutorials/installing-packages/#creating-and-using-virtual-environments)
could be used, which separates the installation from your global and user wide
Python packages.

It would also be possible to use [pipenv] for the management of the virtual
environment but the activation of the environment has to be done manually.

Therefore it would be even possible to run different versions of autohooks by
using the `pythonpath` mode and switching a virtual environment.

### Pipenv mode

In the `pipenv` mode [pipenv] is used to run autohooks in a dedicated virtual
environment. Pipenv uses a lockfile to install exact versions. Therefore the
installation is deterministic and reliable between different developer setups.
In contrast to the `pythonpath` mode the activation of the virtual environment
provided by [pipenv] is done automatically in the background.

Using the `pipenv` mode is highly recommended.

## Installation

For the installation of autohooks three steps are necessary:

1. Install the autohooks package into your current environment
2. Activate the [git hooks](https://git-scm.com/docs/githooks)
3. Configure the plugins to be run
1. Install autohooks package into your current environment
3. Configure mode and plugins to be run
2. Activate [git hooks](https://git-scm.com/docs/githooks)

### Install autohooks python package

For installing the autohooks python package, using
[pipenv](https://pipenv.readthedocs.io/) is highly recommended.
For installing the autohooks python package, using [pipenv] is highly
recommended.

To install autohooks as a development dependency run

Expand Down Expand Up @@ -88,35 +129,25 @@ autohooks = {git = "https://github.com/greenbone/autohooks"}

to the `[dev-packages]` section of your `Pipfile`.

### Activating the git hooks

If autohooks is installed from git or a source tarball, the git hooks should be
activated automatically. The activation can be verified by running e.g.
`autohooks check`.
### Configure mode and plugins to be run

Installing autohooks from a [wheel](https://www.python.org/dev/peps/pep-0427/)
package will **NOT** activate the git commit hooks.

To manually activate the git hooks you can run

```sh
pipenv run autohooks activate
```
Autohooks uses the *pyproject.toml* file specified in
[PEP518](https://www.python.org/dev/peps/pep-0518/) for its configuration.
Adding a *[tool.autohooks]* section allows to specify the desired [autohooks mode](#modes)
and to set python modules to be run as [autohooks plugin](#plugins).

### Configure plugins to be run
The mode can be set by adding a `mode =` line to the *pyproject.toml* file.
Current possible options are `"pythonpath"` and `"pipenv".`See
[autohooks mode](#modes) for more details. If the mode setting is missing it
falls back to `pythonpath` mode.

To actually run an action on git hooks, [autohooks plugins](#plugins) have to be
installed and configured. To install e.g. python linting via pylint run

```
```bash
pipenv install --dev autohooks-plugin-pylint
```

Autohooks uses the *pyproject.toml* file specified in
[PEP518](https://www.python.org/dev/peps/pep-0518/) for its configuration.
Adding a *[tool.autohooks]* section allows to set python modules to be run on a
specific git hook.

Example *pyproject.toml*:

```toml
Expand All @@ -125,49 +156,31 @@ requires = ["setuptools", "wheel"]

[tool.autohooks]
pre-commit = ["autohooks.plugins.black"]
mode = "pipenv"
```

## Proposed Workflow

Using [pipenv](https://pipenv.readthedocs.io/) allows to install all
dependencies and tools with a specific version into a virtual, easily removable
Python environment. Therefore it's best to maintain **autohooks** also via
pipenv. Because it is not required to build or run your software, it should be
[installed as a development dependency](#install-autohooks-python-package).
Installing and [activating](#activating-the-git-hooks) autohooks doesn't
actually run any check or formatting by itself. Therefore, it is required to
[choose and install a plugin](#configure-plugins-to-be-run).
### Activating the git hooks

If all these tasks have been resolved, the developers are able to install
and activate autohooks with only one single command from your project's git
repository:
If autohooks is installed from git or a source tarball, the git hooks should be
activated automatically. The activation can be verified by running e.g.
`autohooks check`.

```sh
pipenv install --dev
```
Installing autohooks from a [wheel](https://www.python.org/dev/peps/pep-0427/)
package will **NOT** activate the git commit hooks automatically.

Because virtual environments are used for all dependencies including
autohooks, the linting, formatting, etc. can only by done when running
`git commit` within the virtual environment.
To manually activate the git hooks you can run

```sh
$ cd myproject
$ pipenv install --dev
$ pipenv shell
(myproject)$ git commit
```bash
pipenv run autohooks activate
```

The advantage of this process is, if the user is not running `git commit` within
the active virtual environment, autohooks and its plugins are not executed.
Calling `activate` also allows for overriding the [mode](#modes) defined in the
*pyproject.toml* settings. E.g.

```sh
$ cd myproject
$ git commit
```bash
pipenv run autohooks activate --mode pipenv
```

This allows the user to choose whether to execute the hooks by activating the
virtual environment or to ignore them by deactivating it.

## Plugins

* Python code formatting via [black](https://github.com/greenbone/autohooks-plugin-black)
Expand All @@ -181,7 +194,7 @@ virtual environment or to ignore them by deactivating it.
Plugins need to be available in the
[Python import path](https://docs.python.org/3/reference/import.html). The
easiest way to achieve this, is to upload a plugin to [PyPI](https://pypi.org/)
and install it via [pip]() or [pipenv](http://pipenv.readthedocs.io/).
and install it via [pip] or [pipenv].

Alternatively, a plugin can also be put into a *.autohooks* directory at the root
directory of the git repository where the hooks should be executed.
Expand Down Expand Up @@ -349,3 +362,6 @@ first.
Copyright (C) 2019 [Greenbone Networks GmbH](https://www.greenbone.net/)

Licensed under the [GNU General Public License v3.0 or later](LICENSE).

[pip]: https://pip.pypa.io/
[pipenv]: https://pipenv.readthedocs.io/
9 changes: 9 additions & 0 deletions autohooks/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import argparse

from autohooks.setting import Mode
from autohooks.version import get_version

from autohooks.cli.activate import install_hooks
Expand All @@ -43,6 +44,14 @@ def main():
action='store_true',
help='Force activation of hook even if a hook already exists',
)
activate_parser.add_argument(
'-m',
'--mode',
dest='mode',
choices=[str(Mode.PYTHONPATH), str(Mode.PIPENV)],
help='Mode for loading autohooks during hook execution. Either load '
'autohooks from the PYTHON_PATH or via pipenv.',
)

subparsers.add_parser('check', help='Check installed pre-commit hook')

Expand Down
12 changes: 10 additions & 2 deletions autohooks/cli/activate.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
get_pre_commit_hook_path,
get_autohooks_pre_commit_hook,
)
from autohooks.setting import Mode


def install_hooks(args):
Expand All @@ -49,9 +50,16 @@ def install_hooks(args):
file=sys.stderr,
)

autohooks_pre_commit_hook = get_autohooks_pre_commit_hook()
if args.mode:
mode = Mode.from_string(args.mode)
else:
mode = config.get_mode()

autohooks_pre_commit_hook = get_autohooks_pre_commit_hook(mode)
install_pre_commit_hook(autohooks_pre_commit_hook, pre_commit_hook_path)

print(
'pre-commit hook installed at {}'.format(str(pre_commit_hook_path))
'pre-commit hook installed at {} using {} mode'.format(
str(pre_commit_hook_path), str(mode.get_effective_mode())
)
)
8 changes: 2 additions & 6 deletions autohooks/cli/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from autohooks.install import (
get_pre_commit_hook_path,
get_pre_commit_hook_template_path,
is_autohooks_pre_commit_hook,
)

from autohooks.config import (
Expand All @@ -38,13 +38,9 @@

def check_hooks():
pre_commit_hook = get_pre_commit_hook_path()
pre_commit_template = get_pre_commit_hook_template_path()

template = pre_commit_template.read_text()

if pre_commit_hook.is_file():
hook = pre_commit_hook.read_text()
if hook == template:
if is_autohooks_pre_commit_hook(pre_commit_hook):
ok('autohooks pre-commit hook is active.')
else:
error(
Expand Down
14 changes: 14 additions & 0 deletions autohooks/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import toml

from autohooks.setting import Mode
from autohooks.utils import get_pyproject_toml_path

AUTOHOOKS_SECTION = 'tool.autohooks'
Expand Down Expand Up @@ -61,6 +62,19 @@ def get_pre_commit_script_names(self):

return []

def get_mode(self):
if self.has_autohooks_config():
mode = self._autohooks_config.get_value('mode')
if not mode:
return Mode.UNDEFINED

try:
return Mode[mode.upper()]
except KeyError:
return Mode.UNKNOWN

return Mode.UNDEFINED

def get_config(self):
return self._config

Expand Down
28 changes: 17 additions & 11 deletions autohooks/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,43 @@
from setuptools.command.install import install
from setuptools.command.develop import develop

from autohooks.utils import (
get_git_hook_directory_path,
get_autohooks_directory_path,
)
from autohooks.config import load_config_from_pyproject_toml
from autohooks.template import PreCommitTemplate

from autohooks.utils import get_git_hook_directory_path


def get_pre_commit_hook_path():
git_hook_dir_path = get_git_hook_directory_path()
return git_hook_dir_path / 'pre-commit'


def get_pre_commit_hook_template_path():
setup_dir_path = get_autohooks_directory_path()
return setup_dir_path / 'precommit' / 'template'
def get_autohooks_pre_commit_hook(mode):
template = PreCommitTemplate()

return template.render(mode=mode)


def get_autohooks_pre_commit_hook():
template_path = get_pre_commit_hook_template_path()
return template_path.read_text()
def is_autohooks_pre_commit_hook(path):
hook = path.read_text()
lines = hook.split('\n')
return len(lines) > 5 and "autohooks.precommit" in lines[5]


def install_pre_commit_hook(pre_commit_hook, pre_commit_hook_path):
pre_commit_hook_path.write_text(pre_commit_hook)
pre_commit_hook_path.chmod(0o775)


class AutohooksInstall:
def install_git_hook(self):
try:
pre_commit_hook_path = get_pre_commit_hook_path()
if not pre_commit_hook_path.exists():
autohooks_pre_commit_hook = get_autohooks_pre_commit_hook()
config = load_config_from_pyproject_toml()

mode = config.get_mode()
autohooks_pre_commit_hook = get_autohooks_pre_commit_hook(mode)
install_pre_commit_hook(
autohooks_pre_commit_hook, pre_commit_hook_path
)
Expand Down
Loading