Skip to content

Commit

Permalink
Merge pull request #24 from bjoernricks/config-mode
Browse files Browse the repository at this point in the history
Add new mode autohooks config setting
  • Loading branch information
bjoernricks authored Sep 13, 2019
2 parents c49fdc5 + 96cafaf commit 181ccf9
Show file tree
Hide file tree
Showing 14 changed files with 468 additions and 98 deletions.
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.

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

0 comments on commit 181ccf9

Please sign in to comment.