cz_keep_a_changelog_plugin
Adds full compliance of Commitizen with Keep a Changelog v1.1.0.
Features
- Complete map of
Conventional Commits-types
to Keep a Changelog-types of changes:
feat:Addedfix:Fixedperf:Changedrefactor:Changedrevert:Removed
- Enables to replace issue numbers in the changelog by a markdown link to the
issue:
#123->[#123](https://github.com/user/repo/issues/123) - Keep a Changelog-like header for changelog file.
Installation
Using pip:
# Install
pip install cz-keep-a-changelog-plugin
# Keep it updated
pip upgrade cz-keep-a-changelog-plugin
Using uv:
# Install
uv install cz-keep-a-changelog-plugin
# Keep it updated
uv upgrade cz-keep-a-changelog-plugin
Package name uses -!
Usage
Configuration
In the project's pyproject.toml set:
...
[tool.commitizen]
name = "cz_keep_a_changelog_plugin"
...
[tool.commitizen.cz_keep_a_changelog_plugin]
issue_url_template = "https://cola5.github.io/cz-keep-a-changelog-plugin/issues/{{issue}}"
Plugin name uses _!
Pre-Commit
To use in pre-commit, add this to your pre-commit-config.yml. Run pre-commit
autoupdate to get the latest version:
repos:
- repo: https://github.com/commitizen-tools/commitizen
rev: main
hooks:
- id: commitizen
additional_dependencies:
- cz-keep-a-changelog-plugin==<VERSION>
stages: [commit-msg]
- id: commitizen-branch
additional_dependencies:
- cz-keep-a-changelog-plugin==<VERSION>
stages: [pre-push]
Package name uses -!
1""" 2.. include:: ../README.md 3 :end-before: # Commitizen - Keep a Changelog-Plugin 4 5--- 6 7.. include:: ../README.md 8 :start-after: # Commitizen - Keep a Changelog-Plugin 9 10--- 11""" # noqa: D212, D415 12 13from cz_keep_a_changelog_plugin.cz_keep_a_changelog_plugin import ( 14 CzKeepAChangelogPlugin, 15) 16from cz_keep_a_changelog_plugin.cz_keep_a_changelog_plugin import ( 17 CzPluginSettings, 18) 19from cz_keep_a_changelog_plugin.version import ( 20 CZ_KEEP_A_CHANGELOG_PLUGIN_VERSION, 21) 22 23__docformat__ = "google" 24__license__ = "MIT" 25__version__ = CZ_KEEP_A_CHANGELOG_PLUGIN_VERSION 26 27__all__ = ( 28 "CzKeepAChangelogPlugin", 29 "CzPluginSettings", 30 "__license__", 31 "__version__", 32)
API Documentation
41class CzKeepAChangelogPlugin(ConventionalCommitsCz): 42 """'Keep a Changelog'-Conventional Commits-Plugin.""" 43 44 bump_pattern: str | None = r"^(\w+(\(.+\))?!?):" 45 bump_map: dict[str, str] | None = OrderedDict( 46 ( 47 (r"^.+!$", MAJOR), 48 (r"^feat", MINOR), 49 (r"^revert", MINOR), 50 (r"^fix", PATCH), 51 (r"^perf", PATCH), 52 (r"^refactor", PATCH), 53 ) 54 ) 55 bump_map_major_version_zero: dict[str, str] | None = OrderedDict( 56 ( 57 (r"^.+!$", MINOR), 58 (r"^feat", MINOR), 59 (r"^revert", MINOR), 60 (r"^fix", PATCH), 61 (r"^perf", PATCH), 62 (r"^refactor", PATCH), 63 ) 64 ) 65 66 commit_parser: str | None = ( 67 r"^((?P<change_type>feat|fix|perf|refactor|revert)" 68 r"(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?|\w+!):" 69 r"\s(?P<message>.*)?" 70 ) 71 changelog_pattern: str | None = bump_pattern 72 change_type_map: dict[str, str] | None = { 73 "feat": "Added", 74 "fix": "Fixed", 75 "perf": "Changed", 76 "refactor": "Changed", 77 "revert": "Removed", 78 } 79 change_type_order: list[str] | None = [ 80 "Added", 81 "Changed", 82 "Deprecated", 83 "Removed", 84 "Fixed", 85 "Security", 86 ] 87 88 template_header: str = "CHANGELOG_HEADER.md.j2" 89 template_loader: j2.BaseLoader = j2.PackageLoader( 90 "cz_keep_a_changelog_plugin", "templates" 91 ) 92 93 def __init__(self, config: BaseConfig) -> None: 94 """Initializes the plugin. 95 96 Args: 97 config: The base config of `commitizen`. 98 """ 99 super().__init__(config) 100 self.plugin_settings: CzPluginSettings = self.config.settings.get( 101 "cz_keep_a_changelog_plugin", {} 102 ) 103 """The plugin settings set in TOML.""" 104 105 def changelog_hook( 106 self, 107 changelog_out: str, 108 partial_changelog: str | None, 109 ) -> str: 110 """Customizes the complete changelog or uses it to call further 111 functionality. 112 113 Here, the header will be added if not done before. 114 115 Args: 116 changelog_out: The full changelog. 117 partial_changelog: The partial (if used `incremental`) changelog, 118 else `None`. 119 120 Returns: 121 Must return the complete changelog. 122 """ 123 loader = j2.ChoiceLoader( 124 [j2.FileSystemLoader("."), self.template_loader] 125 ) 126 env = j2.Environment(loader=loader, trim_blocks=True) 127 jinja_template_header = env.get_template(self.template_header) 128 header = jinja_template_header.render( 129 **self.template_extras, 130 **self.config.settings["extras"], 131 ) 132 header = header.strip() 133 if changelog_out.startswith(header): 134 return changelog_out 135 return "\n\n".join((header, changelog_out)) 136 137 def changelog_message_builder_hook( 138 self, 139 message: dict[str, Any], 140 commit: git.GitCommit, 141 ) -> Iterable[dict[str, Any]] | dict[str, Any] | None: 142 """Customizes the changelog message with extra information, like adding 143 links. 144 145 This function is executed per parsed commit. Each `GitCommit` contains 146 the following attrs: `rev`, `title`, `body`, `author`, `author_email`. 147 148 Here, the issue number is replaced by a markdown link using the issue 149 url template. 150 151 Args: 152 message: The message to customize. 153 commit: The original `GitCommit` which contains the following attrs: 154 `rev`, `title`, `body`, `author`, `author_email`. 155 156 Returns: 157 A customized message or a falsy value to ignore the commit. 158 """ 159 if "issue_url_template" in self.plugin_settings: 160 issue_url_template = j2.Template( 161 self.plugin_settings["issue_url_template"] 162 ) 163 message["message"] = replace_github_issues( 164 message["message"], issue_url_template 165 ) 166 return message 167 168 def questions(self) -> list[CzQuestion]: 169 """Returns the questions regarding the commit message. 170 171 Returns: 172 The questions. 173 """ 174 return [ 175 { 176 "type": "list", 177 "name": "prefix", 178 "message": "Select the type of change you are committing", 179 "choices": [ 180 { 181 "value": "feat", 182 "name": ( 183 "feat: A new feature. Correlates with MINOR in " 184 "SemVer" 185 ), 186 "key": "f", 187 }, 188 { 189 "value": "fix", 190 "name": ( 191 "fix: A bug fix. Correlates with PATCH in SemVer" 192 ), 193 "key": "x", 194 }, 195 { 196 "value": "docs", 197 "name": "docs: Documentation only changes", 198 "key": "d", 199 }, 200 { 201 "value": "style", 202 "name": ( 203 "style: Changes that do not affect the meaning of " 204 "the code (white-space, formatting, missing " 205 "semi-colons, etc)" 206 ), 207 "key": "s", 208 }, 209 { 210 "value": "refactor", 211 "name": ( 212 "refactor: A code change that neither fixes a bug " 213 "nor adds a feature" 214 ), 215 "key": "r", 216 }, 217 { 218 "value": "perf", 219 "name": "perf: A code change that improves performance", 220 "key": "p", 221 }, 222 { 223 "value": "test", 224 "name": ( 225 "test: Adding missing tests or correcting existing " 226 "tests" 227 ), 228 "key": "t", 229 }, 230 { 231 "value": "build", 232 "name": ( 233 "build: Changes that affect the build system or " 234 "external dependencies (example scopes: pip, " 235 "docker, npm)" 236 ), 237 "key": "b", 238 }, 239 { 240 "value": "ci", 241 "name": ( 242 "ci: Changes to CI configuration files and scripts " 243 "(example scopes: GitLabCI)" 244 ), 245 "key": "c", 246 }, 247 { 248 "value": "chore", 249 "name": ( 250 "Other changes that don't modify src or test files" 251 ), 252 "key": "h", 253 }, 254 { 255 "value": "revert", 256 "name": ( 257 "Reverts a previous commit. Put reverted commit " 258 "SHAs into footer as'Refs: 676104e, a215868'" 259 ), 260 "key": "v", 261 }, 262 ], 263 }, 264 { 265 "type": "input", 266 "name": "scope", 267 "message": ( 268 "What is the scope of this change? (class or file name): " 269 "(press [enter] to skip)\n" 270 ), 271 "filter": _parse_scope, 272 }, 273 { 274 "type": "input", 275 "name": "subject", 276 "filter": _parse_subject, 277 "message": ( 278 "Write a short and imperative summary of the code changes: " 279 "(lower case and no period)\n" 280 ), 281 }, 282 { 283 "type": "input", 284 "name": "body", 285 "message": ( 286 "Provide additional contextual information about the code " 287 "changes: (press [enter] to skip)\n" 288 ), 289 "filter": multiple_line_breaker, 290 }, 291 { 292 "type": "confirm", 293 "name": "is_breaking_change", 294 "message": ( 295 "Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer" 296 ), 297 "default": False, 298 }, 299 { 300 "type": "input", 301 "name": "footer", 302 "message": ( 303 "Footer. Information about Breaking Changes and reference " 304 "issues that this commit closes: (press [enter] to skip)\n" 305 ), 306 }, 307 ]
'Keep a Changelog'-Conventional Commits-Plugin.
93 def __init__(self, config: BaseConfig) -> None: 94 """Initializes the plugin. 95 96 Args: 97 config: The base config of `commitizen`. 98 """ 99 super().__init__(config) 100 self.plugin_settings: CzPluginSettings = self.config.settings.get( 101 "cz_keep_a_changelog_plugin", {} 102 ) 103 """The plugin settings set in TOML."""
Initializes the plugin.
Arguments:
- config: The base config of
commitizen.
105 def changelog_hook( 106 self, 107 changelog_out: str, 108 partial_changelog: str | None, 109 ) -> str: 110 """Customizes the complete changelog or uses it to call further 111 functionality. 112 113 Here, the header will be added if not done before. 114 115 Args: 116 changelog_out: The full changelog. 117 partial_changelog: The partial (if used `incremental`) changelog, 118 else `None`. 119 120 Returns: 121 Must return the complete changelog. 122 """ 123 loader = j2.ChoiceLoader( 124 [j2.FileSystemLoader("."), self.template_loader] 125 ) 126 env = j2.Environment(loader=loader, trim_blocks=True) 127 jinja_template_header = env.get_template(self.template_header) 128 header = jinja_template_header.render( 129 **self.template_extras, 130 **self.config.settings["extras"], 131 ) 132 header = header.strip() 133 if changelog_out.startswith(header): 134 return changelog_out 135 return "\n\n".join((header, changelog_out))
Customizes the complete changelog or uses it to call further functionality.
Here, the header will be added if not done before.
Arguments:
- changelog_out: The full changelog.
- partial_changelog: The partial (if used
incremental) changelog, elseNone.
Returns:
Must return the complete changelog.
137 def changelog_message_builder_hook( 138 self, 139 message: dict[str, Any], 140 commit: git.GitCommit, 141 ) -> Iterable[dict[str, Any]] | dict[str, Any] | None: 142 """Customizes the changelog message with extra information, like adding 143 links. 144 145 This function is executed per parsed commit. Each `GitCommit` contains 146 the following attrs: `rev`, `title`, `body`, `author`, `author_email`. 147 148 Here, the issue number is replaced by a markdown link using the issue 149 url template. 150 151 Args: 152 message: The message to customize. 153 commit: The original `GitCommit` which contains the following attrs: 154 `rev`, `title`, `body`, `author`, `author_email`. 155 156 Returns: 157 A customized message or a falsy value to ignore the commit. 158 """ 159 if "issue_url_template" in self.plugin_settings: 160 issue_url_template = j2.Template( 161 self.plugin_settings["issue_url_template"] 162 ) 163 message["message"] = replace_github_issues( 164 message["message"], issue_url_template 165 ) 166 return message
Customizes the changelog message with extra information, like adding links.
This function is executed per parsed commit. Each GitCommit contains
the following attrs: rev, title, body, author, author_email.
Here, the issue number is replaced by a markdown link using the issue url template.
Arguments:
- message: The message to customize.
- commit: The original
GitCommitwhich contains the following attrs:rev,title,body,author,author_email.
Returns:
A customized message or a falsy value to ignore the commit.
168 def questions(self) -> list[CzQuestion]: 169 """Returns the questions regarding the commit message. 170 171 Returns: 172 The questions. 173 """ 174 return [ 175 { 176 "type": "list", 177 "name": "prefix", 178 "message": "Select the type of change you are committing", 179 "choices": [ 180 { 181 "value": "feat", 182 "name": ( 183 "feat: A new feature. Correlates with MINOR in " 184 "SemVer" 185 ), 186 "key": "f", 187 }, 188 { 189 "value": "fix", 190 "name": ( 191 "fix: A bug fix. Correlates with PATCH in SemVer" 192 ), 193 "key": "x", 194 }, 195 { 196 "value": "docs", 197 "name": "docs: Documentation only changes", 198 "key": "d", 199 }, 200 { 201 "value": "style", 202 "name": ( 203 "style: Changes that do not affect the meaning of " 204 "the code (white-space, formatting, missing " 205 "semi-colons, etc)" 206 ), 207 "key": "s", 208 }, 209 { 210 "value": "refactor", 211 "name": ( 212 "refactor: A code change that neither fixes a bug " 213 "nor adds a feature" 214 ), 215 "key": "r", 216 }, 217 { 218 "value": "perf", 219 "name": "perf: A code change that improves performance", 220 "key": "p", 221 }, 222 { 223 "value": "test", 224 "name": ( 225 "test: Adding missing tests or correcting existing " 226 "tests" 227 ), 228 "key": "t", 229 }, 230 { 231 "value": "build", 232 "name": ( 233 "build: Changes that affect the build system or " 234 "external dependencies (example scopes: pip, " 235 "docker, npm)" 236 ), 237 "key": "b", 238 }, 239 { 240 "value": "ci", 241 "name": ( 242 "ci: Changes to CI configuration files and scripts " 243 "(example scopes: GitLabCI)" 244 ), 245 "key": "c", 246 }, 247 { 248 "value": "chore", 249 "name": ( 250 "Other changes that don't modify src or test files" 251 ), 252 "key": "h", 253 }, 254 { 255 "value": "revert", 256 "name": ( 257 "Reverts a previous commit. Put reverted commit " 258 "SHAs into footer as'Refs: 676104e, a215868'" 259 ), 260 "key": "v", 261 }, 262 ], 263 }, 264 { 265 "type": "input", 266 "name": "scope", 267 "message": ( 268 "What is the scope of this change? (class or file name): " 269 "(press [enter] to skip)\n" 270 ), 271 "filter": _parse_scope, 272 }, 273 { 274 "type": "input", 275 "name": "subject", 276 "filter": _parse_subject, 277 "message": ( 278 "Write a short and imperative summary of the code changes: " 279 "(lower case and no period)\n" 280 ), 281 }, 282 { 283 "type": "input", 284 "name": "body", 285 "message": ( 286 "Provide additional contextual information about the code " 287 "changes: (press [enter] to skip)\n" 288 ), 289 "filter": multiple_line_breaker, 290 }, 291 { 292 "type": "confirm", 293 "name": "is_breaking_change", 294 "message": ( 295 "Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer" 296 ), 297 "default": False, 298 }, 299 { 300 "type": "input", 301 "name": "footer", 302 "message": ( 303 "Footer. Information about Breaking Changes and reference " 304 "issues that this commit closes: (press [enter] to skip)\n" 305 ), 306 }, 307 ]
Returns the questions regarding the commit message.
Returns:
The questions.
35class CzPluginSettings(TypedDict, total=False): 36 """Plugin Settings.""" 37 38 issue_url_template: str
Plugin Settings.