# Customizing through a python class
The basic steps are:
1. Inheriting from `BaseCommitizen`.
2. Give a name to your rules.
3. Create a Python package using proper [build backend](https://packaging.python.org/en/latest/glossary/#term-Build-Backend)
4. Expose the class as a `commitizen.plugin` entrypoint.
Check an [example][convcomms] on how to configure `BaseCommitizen`.
You can also automate the steps above through [cookiecutter](https://cookiecutter.readthedocs.io/en/1.7.0/).
```sh
cookiecutter gh:commitizen-tools/commitizen_cz_template
```
See [commitizen_cz_template](https://github.com/commitizen-tools/commitizen_cz_template) for details.
See [Third-party plugins](../third-party-plugins/about.md) for more details on how to create a third-party Commitizen plugin.
## Custom commit rules
Create a Python module, for example `cz_jira.py`.
Inherit from `BaseCommitizen`, and you must define `questions` and `message`. The others are optional.
```python title="cz_jira.py"
from commitizen.cz.base import BaseCommitizen
from commitizen.defaults import Questions
class JiraCz(BaseCommitizen):
# Questions = Iterable[MutableMapping[str, Any]]
# It expects a list with dictionaries.
def questions(self) -> Questions:
"""Questions regarding the commit message."""
questions = [
{"type": "input", "name": "title", "message": "Commit title"},
{"type": "input", "name": "issue", "message": "Jira Issue number:"},
]
return questions
def message(self, answers: dict) -> str:
"""Generate the message with the given answers."""
return f"answers['title'] (#answers['issue'])"
def example(self) -> str:
"""Provide an example to help understand the style (OPTIONAL)
Used by `cz example`.
"""
return "Problem with user (#321)"
def schema(self) -> str:
"""Show the schema used (OPTIONAL)
Used by `cz schema`.
"""
return "
()"
def info(self) -> str:
"""Explanation of the commit rules. (OPTIONAL)
Used by `cz info`.
"""
return "We use this because is useful"
```
The next file required is `setup.py` modified from flask version.
```python title="setup.py"
from setuptools import setup
setup(
name="JiraCommitizen",
version="0.1.0",
py_modules=["cz_jira"],
license="MIT",
long_description="this is a long description",
install_requires=["commitizen"],
entry_points={"commitizen.plugin": ["cz_jira = cz_jira:JiraCz"]},
)
```
So in the end, we would have
.
├── cz_jira.py
└── setup.py
And that's it. You can install it without uploading to PyPI by simply
doing `pip install .`
## Custom bump rules
You need to define 2 parameters inside your custom `BaseCommitizen`.
| Parameter | Type | Default | Description |
| -------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- |
| `bump_pattern` | `str` | `None` | Regex to extract information from commit (subject and body) |
| `bump_map` | `dict` | `None` | Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) |
Let's see an example.
```python title="cz_strange.py"
from commitizen.cz.base import BaseCommitizen
class StrangeCommitizen(BaseCommitizen):
bump_pattern = r"^(break|new|fix|hotfix)"
bump_map = {"break": "MAJOR", "new": "MINOR", "fix": "PATCH", "hotfix": "PATCH"}
```
That's it, your Commitizen now supports custom rules, and you can run.
```bash
cz -n cz_strange bump
```
[convcomms]: https://github.com/commitizen-tools/commitizen/blob/master/commitizen/cz/conventional_commits/conventional_commits.py
### Custom commit validation and error message
The commit message validation can be customized by overriding the `validate_commit_message` and `format_error_message`
methods from `BaseCommitizen`. This allows for a more detailed feedback to the user where the error originates from.
```python
import re
from commitizen.cz.base import BaseCommitizen, ValidationResult
from commitizen import git
class CustomValidationCz(BaseCommitizen):
def validate_commit_message(
self,
*,
commit_msg: str,
pattern: str | None,
allow_abort: bool,
allowed_prefixes: list[str],
max_msg_length: int,
) -> ValidationResult:
"""Validate commit message against the pattern."""
if not commit_msg:
return allow_abort, [] if allow_abort else [f"commit message is empty"]
if pattern is None:
return True, []
if any(map(commit_msg.startswith, allowed_prefixes)):
return True, []
if max_msg_length:
msg_len = len(commit_msg.partition("\n")[0].strip())
if msg_len > max_msg_length:
return False, [
f"commit message is too long. Max length is {max_msg_length}"
]
pattern_match = re.match(pattern, commit_msg)
if pattern_match:
return True, []
else:
# Perform additional validation of the commit message format
# and add custom error messages as needed
return False, ["commit message does not match the pattern"]
def format_exception_message(
self, ill_formatted_commits: list[tuple[git.GitCommit, list]]
) -> str:
"""Format commit errors."""
displayed_msgs_content = "\n".join(
(
f'commit "{commit.rev}": "{commit.message}"'
f"errors:\n"
"\n".join((f"- {error}" for error in errors))
)
for commit, errors in ill_formatted_commits
)
return (
"commit validation: failed!\n"
"please enter a commit message in the commitizen format.\n"
f"{displayed_msgs_content}\n"
f"pattern: {self.schema_pattern()}"
)
```
## Custom changelog generator
The changelog generator should just work in a very basic manner without touching anything.
You can customize it of course, and the following variables are the ones you need to add to your custom `BaseCommitizen`.
| Parameter | Type | Required | Description |
| -------------------------------- | ------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `commit_parser` | `str` | NO | Regex which should provide the variables explained in the [changelog description][changelog-des] |
| `changelog_pattern` | `str` | NO | Regex to validate the commits, this is useful to skip commits that don't meet your ruling standards like a Merge. Usually the same as bump_pattern |
| `change_type_map` | `dict` | NO | Convert the title of the change type that will appear in the changelog, if a value is not found, the original will be provided |
| `changelog_message_builder_hook` | `method: (dict, git.GitCommit) -> dict | list | None` | NO | Customize with extra information your message output, like adding links, this function is executed per parsed commit. Each GitCommit contains the following attrs: `rev`, `title`, `body`, `author`, `author_email`. Returning a falsy value ignore the commit. |
| `changelog_hook` | `method: (full_changelog: str, partial_changelog: Optional[str]) -> str` | NO | Receives the whole and partial (if used incremental) changelog. Useful to send slack messages or notify a compliance department. Must return the full_changelog |
| `changelog_release_hook` | `method: (release: dict, tag: git.GitTag) -> dict` | NO | Receives each generated changelog release and its associated tag. Useful to enrich releases before they are rendered. Must return the update release
```python title="cz_strange.py"
from commitizen.cz.base import BaseCommitizen
import chat
import compliance
class StrangeCommitizen(BaseCommitizen):
changelog_pattern = r"^(break|new|fix|hotfix)"
commit_parser = r"^(?Pfeat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?:\s(?P.*)?"
change_type_map = {
"feat": "Features",
"fix": "Bug Fixes",
"refactor": "Code Refactor",
"perf": "Performance improvements",
}
def changelog_message_builder_hook(
self, parsed_message: dict, commit: git.GitCommit
) -> dict | list | None:
rev = commit.rev
m = parsed_message["message"]
parsed_message[
"message"
] = f"{m} {rev} [{commit.author}]({commit.author_email})"
return parsed_message
def changelog_release_hook(self, release: dict, tag: git.GitTag) -> dict:
release["author"] = tag.author
return release
def changelog_hook(
self, full_changelog: str, partial_changelog: Optional[str]
) -> str:
"""Executed at the end of the changelog generation
full_changelog: it's the output about to being written into the file
partial_changelog: it's the new stuff, this is useful to send slack messages or
similar
Return:
the new updated full_changelog
"""
if partial_changelog:
chat.room("#committers").notify(partial_changelog)
if full_changelog:
compliance.send(full_changelog)
full_changelog.replace(" fix ", " **fix** ")
return full_changelog
```
## Raise Customize Exception
If you want `commitizen` to catch your exception and print the message, you'll have to inherit `CzException`.
```python
from commitizen.cz.exception import CzException
class NoSubjectProvidedException(CzException):
...
```
## Migrating from legacy plugin format
Commitizen migrated to a new plugin format relying on `importlib.metadata.EntryPoint`.
Migration should be straight-forward for legacy plugins:
- Remove the `discover_this` line from your plugin module
- Expose the plugin class under as a `commitizen.plugin` entrypoint.
The name of the plugin is now determined by the name of the entrypoint.
### Example
If you were having a `CzPlugin` class in a `cz_plugin.py` module like this:
```python
from commitizen.cz.base import BaseCommitizen
class PluginCz(BaseCommitizen):
...
discover_this = PluginCz
```
Then remove the `discover_this` line:
```python
from commitizen.cz.base import BaseCommitizen
class PluginCz(BaseCommitizen):
...
```
and expose the class as entrypoint in your `setuptools`:
```python
from setuptools import setup
setup(
name="MyPlugin",
version="0.1.0",
py_modules=["cz_plugin"],
entry_points={"commitizen.plugin": ["plugin = cz_plugin:PluginCz"]},
...,
)
```
Then your plugin will be available under the name `plugin`.