Customizing through a python class¶
The basic steps are:
- Inheriting from
BaseCommitizen. - Give a name to your rules.
- Create a Python package using properbuild backend
- Expose the class as a
commitizen.pluginentrypoint.
Check anexample on how to configureBaseCommitizen.
You can also automate the steps above throughcookiecutter.
cookiecuttergh:commitizen-tools/commitizen_cz_templateSeecommitizen_cz_template for details.
SeeThird-party plugins for more details on how to create a third-party Commitizen plugin.
Custom commit rules¶
Create a Python module, for examplecz_jira.py.
Inherit fromBaseCommitizen, and you must definequestions andmessage. The others are optional.
fromcommitizen.cz.baseimportBaseCommitizenfromcommitizen.defaultsimportQuestionsclassJiraCz(BaseCommitizen):# Questions = Iterable[MutableMapping[str, Any]]# It expects a list with dictionaries.defquestions(self)->Questions:"""Questions regarding the commit message."""questions=[{"type":"input","name":"title","message":"Commit title"},{"type":"input","name":"issue","message":"Jira Issue number:"},]returnquestionsdefmessage(self,answers:dict)->str:"""Generate the message with the given answers."""returnf"answers['title'] (#answers['issue'])"defexample(self)->str:"""Provide an example to help understand the style (OPTIONAL) Used by `cz example`. """return"Problem with user (#321)"defschema(self)->str:"""Show the schema used (OPTIONAL) Used by `cz schema`. """return"<title> (<issue>)"definfo(self)->str:"""Explanation of the commit rules. (OPTIONAL) Used by `cz info`. """return"We use this because is useful"The next file required issetup.py modified from flask version.
fromsetuptoolsimportsetupsetup(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.pyAnd that's it. You can install it without uploading to PyPI by simplydoingpip install .
Custom bump rules¶
You need to define 2 parameters inside your customBaseCommitizen.
| 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 aSemVer increment type (MAJOR,MINOR,PATCH) |
Let's see an example.
fromcommitizen.cz.baseimportBaseCommitizenclassStrangeCommitizen(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.
cz-ncz_strangebumpCustom commit validation and error message¶
The commit message validation can be customized by overriding thevalidate_commit_message andformat_error_messagemethods fromBaseCommitizen. This allows for a more detailed feedback to the user where the error originates from.
importrefromcommitizen.cz.baseimportBaseCommitizen,ValidationResultfromcommitizenimportgitclassCustomValidationCz(BaseCommitizen):defvalidate_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."""ifnotcommit_msg:returnallow_abort,[]ifallow_abortelse[f"commit message is empty"]ifpatternisNone:returnTrue,[]ifany(map(commit_msg.startswith,allowed_prefixes)):returnTrue,[]ifmax_msg_length:msg_len=len(commit_msg.partition("\n")[0].strip())ifmsg_len>max_msg_length:returnFalse,[f"commit message is too long. Max length is{max_msg_length}"]pattern_match=re.match(pattern,commit_msg)ifpattern_match:returnTrue,[]else:# Perform additional validation of the commit message format# and add custom error messages as neededreturnFalse,["commit message does not match the pattern"]defformat_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}"forerrorinerrors)))forcommit,errorsinill_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 customBaseCommitizen.
| 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 |
fromcommitizen.cz.baseimportBaseCommitizenimportchatimportcomplianceclassStrangeCommitizen(BaseCommitizen):changelog_pattern=r"^(break|new|fix|hotfix)"commit_parser=r"^(?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?:\s(?P<message>.*)?"change_type_map={"feat":"Features","fix":"Bug Fixes","refactor":"Code Refactor","perf":"Performance improvements",}defchangelog_message_builder_hook(self,parsed_message:dict,commit:git.GitCommit)->dict|list|None:rev=commit.revm=parsed_message["message"]parsed_message["message"]=f"{m}{rev} [{commit.author}]({commit.author_email})"returnparsed_messagedefchangelog_release_hook(self,release:dict,tag:git.GitTag)->dict:release["author"]=tag.authorreturnreleasedefchangelog_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 """ifpartial_changelog:chat.room("#committers").notify(partial_changelog)iffull_changelog:compliance.send(full_changelog)full_changelog.replace(" fix "," **fix** ")returnfull_changelogRaise Customize Exception¶
If you wantcommitizen to catch your exception and print the message, you'll have to inheritCzException.
fromcommitizen.cz.exceptionimportCzExceptionclassNoSubjectProvidedException(CzException):...Migrating from legacy plugin format¶
Commitizen migrated to a new plugin format relying onimportlib.metadata.EntryPoint.Migration should be straight-forward for legacy plugins:
- Remove the
discover_thisline from your plugin module - Expose the plugin class under as a
commitizen.pluginentrypoint.
The name of the plugin is now determined by the name of the entrypoint.
Example¶
If you were having aCzPlugin class in acz_plugin.py module like this:
fromcommitizen.cz.baseimportBaseCommitizenclassPluginCz(BaseCommitizen):...discover_this=PluginCzThen remove thediscover_this line:
fromcommitizen.cz.baseimportBaseCommitizenclassPluginCz(BaseCommitizen):...and expose the class as entrypoint in yoursetuptools:
fromsetuptoolsimportsetupsetup(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 nameplugin.