py-conventional-semver 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_conventional_semver-1.0.0/.scripts/conventional-semver +7 -0
- py_conventional_semver-1.0.0/LICENSE +22 -0
- py_conventional_semver-1.0.0/PKG-INFO +177 -0
- py_conventional_semver-1.0.0/README.md +153 -0
- py_conventional_semver-1.0.0/pyproject.toml +38 -0
- py_conventional_semver-1.0.0/setup.cfg +4 -0
- py_conventional_semver-1.0.0/src/conventional_semver/ChangelogOutputGenerator.py +30 -0
- py_conventional_semver-1.0.0/src/conventional_semver/CommandLineProcessor.py +120 -0
- py_conventional_semver-1.0.0/src/conventional_semver/Configuration.py +160 -0
- py_conventional_semver-1.0.0/src/conventional_semver/GitAdapter.py +67 -0
- py_conventional_semver-1.0.0/src/conventional_semver/GitEntry.py +50 -0
- py_conventional_semver-1.0.0/src/conventional_semver/GitEntryParser.py +58 -0
- py_conventional_semver-1.0.0/src/conventional_semver/GitLogStream.py +40 -0
- py_conventional_semver-1.0.0/src/conventional_semver/OutputGenerator.py +18 -0
- py_conventional_semver-1.0.0/src/conventional_semver/SemverComponentType.py +12 -0
- py_conventional_semver-1.0.0/src/conventional_semver/SemverOutputGenerator.py +55 -0
- py_conventional_semver-1.0.0/src/conventional_semver/__init__.py +29 -0
- py_conventional_semver-1.0.0/src/conventional_semver/__main__.py +41 -0
- py_conventional_semver-1.0.0/src/py_conventional_semver.egg-info/PKG-INFO +177 -0
- py_conventional_semver-1.0.0/src/py_conventional_semver.egg-info/SOURCES.txt +21 -0
- py_conventional_semver-1.0.0/src/py_conventional_semver.egg-info/dependency_links.txt +1 -0
- py_conventional_semver-1.0.0/src/py_conventional_semver.egg-info/requires.txt +7 -0
- py_conventional_semver-1.0.0/src/py_conventional_semver.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
© 2026 Shaun Wilson
|
|
4
|
+
(c) 2024 sw4k
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-conventional-semver
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ..a Python fork of sw4k's `conventional-semver` tool.
|
|
5
|
+
Author-email: Shaun Wilson <mrshaunwilson@msn.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://conventional-semver.readthedocs.io/
|
|
8
|
+
Project-URL: Homepage, https://github.com/wilson0x4d/conventional-semver
|
|
9
|
+
Project-URL: Repository, https://github.com/wilson0x4d/conventional-semver.git
|
|
10
|
+
Keywords: SEMVER,conventional-semver,conventional-commits,sw4k
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: hanaro>=1.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: coverage; extra == "dev"
|
|
20
|
+
Requires-Dist: mypy; extra == "dev"
|
|
21
|
+
Requires-Dist: punit>=1.4.5; extra == "dev"
|
|
22
|
+
Requires-Dist: twine; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
**conventional-semver** is a [Conventional Commits](https://www.conventionalcommits.org/) processor designed to emit a [SEMVER](https://semver.org) as part of a build pipeline.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
Usage:
|
|
32
|
+
conventional-semver [options] [repo-path]
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--help print usage, then exit.
|
|
36
|
+
--version print version info, then exit.
|
|
37
|
+
--verbose enable verbose (debug) output.
|
|
38
|
+
--commit <hash> indicates which commit hash to start changelog from.
|
|
39
|
+
--tag <name> indicates which tag to start changelog from.
|
|
40
|
+
--changelog <file> overrides the name of the file the changelog is written to, otherwise 'changelog.md' is the default.
|
|
41
|
+
--no-semver disable SEMVER output to STDOUT.
|
|
42
|
+
--major SEMVER 'Major' component will start with this value, default is '0'.
|
|
43
|
+
--minor SEMVER 'Minor' component will start with this value, default is '0'.
|
|
44
|
+
--patch SEMVER 'Patch' component will start with this value, default is '0'.
|
|
45
|
+
--git-path <path> overrides the path to `git` tool, otherwise `git` must be in environment PATH.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
repo-path the path of the git repository to process, if not specified defaults to working directory.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Generating SEMVER
|
|
52
|
+
|
|
53
|
+
When you run **conventional-semver** from within a `git` repository, it will automatically process the log and emit a semver.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
% conventional-semver
|
|
59
|
+
0.1.23
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This allows you to pull a semver into an Environment Variable, evaluate it as an Argument to another tool, or pipe it to a file/stream for additional processing:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
% export SEMVER=$(conventional-semver)
|
|
66
|
+
% echo $SEMVER
|
|
67
|
+
0.1.23
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Override Baseline SEMVER
|
|
71
|
+
|
|
72
|
+
Projects adopting Conventional Commits may need to customize the baseline SEMVER, rather than starting from `0.0.0`. This can be done with the `--major`, `--minor`, and `--patch` arguments.
|
|
73
|
+
|
|
74
|
+
When run on a repo containing three non-conventional commits:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
% conventional-semver --major 1 --minor 2 --patch 3
|
|
78
|
+
1.2.6
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### SEMVER Configuration
|
|
82
|
+
|
|
83
|
+
When run without any command-line arguments a default set of settings are used which implement a standard Conventional Commits behavior.
|
|
84
|
+
|
|
85
|
+
To customize behavior a configuration file may be created. This file can be passed in using a `--config` argument, or, placed into one of the following well-known locations (and in the following order):
|
|
86
|
+
|
|
87
|
+
* `./conventional-semver.conf` (working directory.)
|
|
88
|
+
* `~/.config/conventional-semver/settings.conf` (user profile `.config` directory.)
|
|
89
|
+
* `/etc/conventional-semver/settings.conf` (root `/etc` directory.)
|
|
90
|
+
|
|
91
|
+
The configuration file should have the following format:
|
|
92
|
+
|
|
93
|
+
```ini
|
|
94
|
+
# lines starting with hash (#) are comments
|
|
95
|
+
# empty lines, like the following, are ignored
|
|
96
|
+
|
|
97
|
+
# conventional commit "type" mappings are
|
|
98
|
+
# configured in a [types] section. the following
|
|
99
|
+
# mirrors the default configuration:
|
|
100
|
+
[types]
|
|
101
|
+
.*!=major
|
|
102
|
+
feat.*=minor
|
|
103
|
+
.*=patch
|
|
104
|
+
|
|
105
|
+
# conventional commit "footer" mappings are
|
|
106
|
+
# configured in a [footers] section. the following
|
|
107
|
+
# mirrors the default configuration:
|
|
108
|
+
[footers]
|
|
109
|
+
BREAKING[\-\.]CHANGE=major
|
|
110
|
+
|
|
111
|
+
# in each of the above sections, each line
|
|
112
|
+
# represents a key-value pair. the key is a regex
|
|
113
|
+
# and the value is a component type to be
|
|
114
|
+
# incremented if the regex is a match.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
There is a sample configuration file located in this repo as `config/conventional-semver.conf` which contains additional comments and explanations, you can customize it to fit your needs and then place it at one of the well-known locations mentioned above.
|
|
118
|
+
|
|
119
|
+
## Not yet Implemented
|
|
120
|
+
|
|
121
|
+
### Generating CHANGELOG
|
|
122
|
+
|
|
123
|
+
To generate a CHANGELOG you specify the `--changelog [filename]` switch, this takes an optional filename argument. If no filename is provided a default filename of `./CHANGELOG` is used to emit a file into the current working directory.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
% conventional-semver --changelog
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### CHANGELOG Templates
|
|
132
|
+
|
|
133
|
+
The CHANGELOG output is controlled through one or more templates. This includes a required "entry" template, and optional "header" and "footer" templates. Combined this is meant to provide enough flexibility that you could emit templates in various structured formats such as XML, JSON, Markdown, etc.
|
|
134
|
+
|
|
135
|
+
The default templates are meant to produce a generic TEXT file that is markdown-friendly.
|
|
136
|
+
|
|
137
|
+
##### CHANGELOG Entry Template
|
|
138
|
+
|
|
139
|
+
For each SEMVER increment a CHANGELOG Entry is emitted.
|
|
140
|
+
|
|
141
|
+
The format of each CHANGELOG Entry is taken from a file named `changelog-entry.template`, when `--changelog` is specified this file is required to be present or the command will fail.
|
|
142
|
+
|
|
143
|
+
This is the default content of this template:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
$(DATE) $(SEMVER)-$(HASH)
|
|
147
|
+
$(TYPE)$(SCOPE): $(MESSAGE)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The entry template supports the following expando variables:
|
|
151
|
+
|
|
152
|
+
| Expando | Comment |
|
|
153
|
+
|-|-|
|
|
154
|
+
| $(DATE) | The date of the commit which caused SEMVER increment. This is emitted in the default culture of the current environment. |
|
|
155
|
+
| $(SEMVER) | The calculated SEMVER value. |
|
|
156
|
+
| $(HASH) | The short-form git commit hash. |
|
|
157
|
+
| $(TYPE) | The Conventional Commits `<type>` value. |
|
|
158
|
+
| $(SCOPE) | If any, the Conventional Commits `[scope]` value, including parenthesis. |
|
|
159
|
+
| $(MESSAGE) | The commit message, sans `<type>` and `[scope]`. |
|
|
160
|
+
| $(BODY) | If any, the commit body. |
|
|
161
|
+
| $(TRAILERS) | If any, the commit trailers (footers). |
|
|
162
|
+
|
|
163
|
+
##### CHANGELOG Header Template
|
|
164
|
+
|
|
165
|
+
The header prepended to the CHANGELOG comes from a file named `changelog-header.template`.
|
|
166
|
+
|
|
167
|
+
There are no special expando variables supported in the header.
|
|
168
|
+
|
|
169
|
+
##### CHANGELOG Footer Template
|
|
170
|
+
|
|
171
|
+
The footer appended to the CHANGELOG comes from a file named `changelog-footer.template`.
|
|
172
|
+
|
|
173
|
+
There are no special expando variables supported in the footer.
|
|
174
|
+
|
|
175
|
+
##### CHANGELOG Template Caveats
|
|
176
|
+
|
|
177
|
+
No escaping is performed on any of the emitted values. Thus, it is possible for commit messages to interact with parsers/renderers in unintended ways. For example if you make a template to emit an HTML changelog, and a commit message contains content which looks like an 'element', it will most likely result in a changelog that doesn't render as expected.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
|
|
2
|
+
**conventional-semver** is a [Conventional Commits](https://www.conventionalcommits.org/) processor designed to emit a [SEMVER](https://semver.org) as part of a build pipeline.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
Usage:
|
|
8
|
+
conventional-semver [options] [repo-path]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--help print usage, then exit.
|
|
12
|
+
--version print version info, then exit.
|
|
13
|
+
--verbose enable verbose (debug) output.
|
|
14
|
+
--commit <hash> indicates which commit hash to start changelog from.
|
|
15
|
+
--tag <name> indicates which tag to start changelog from.
|
|
16
|
+
--changelog <file> overrides the name of the file the changelog is written to, otherwise 'changelog.md' is the default.
|
|
17
|
+
--no-semver disable SEMVER output to STDOUT.
|
|
18
|
+
--major SEMVER 'Major' component will start with this value, default is '0'.
|
|
19
|
+
--minor SEMVER 'Minor' component will start with this value, default is '0'.
|
|
20
|
+
--patch SEMVER 'Patch' component will start with this value, default is '0'.
|
|
21
|
+
--git-path <path> overrides the path to `git` tool, otherwise `git` must be in environment PATH.
|
|
22
|
+
|
|
23
|
+
Parameters:
|
|
24
|
+
repo-path the path of the git repository to process, if not specified defaults to working directory.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Generating SEMVER
|
|
28
|
+
|
|
29
|
+
When you run **conventional-semver** from within a `git` repository, it will automatically process the log and emit a semver.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
% conventional-semver
|
|
35
|
+
0.1.23
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This allows you to pull a semver into an Environment Variable, evaluate it as an Argument to another tool, or pipe it to a file/stream for additional processing:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
% export SEMVER=$(conventional-semver)
|
|
42
|
+
% echo $SEMVER
|
|
43
|
+
0.1.23
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Override Baseline SEMVER
|
|
47
|
+
|
|
48
|
+
Projects adopting Conventional Commits may need to customize the baseline SEMVER, rather than starting from `0.0.0`. This can be done with the `--major`, `--minor`, and `--patch` arguments.
|
|
49
|
+
|
|
50
|
+
When run on a repo containing three non-conventional commits:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
% conventional-semver --major 1 --minor 2 --patch 3
|
|
54
|
+
1.2.6
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### SEMVER Configuration
|
|
58
|
+
|
|
59
|
+
When run without any command-line arguments a default set of settings are used which implement a standard Conventional Commits behavior.
|
|
60
|
+
|
|
61
|
+
To customize behavior a configuration file may be created. This file can be passed in using a `--config` argument, or, placed into one of the following well-known locations (and in the following order):
|
|
62
|
+
|
|
63
|
+
* `./conventional-semver.conf` (working directory.)
|
|
64
|
+
* `~/.config/conventional-semver/settings.conf` (user profile `.config` directory.)
|
|
65
|
+
* `/etc/conventional-semver/settings.conf` (root `/etc` directory.)
|
|
66
|
+
|
|
67
|
+
The configuration file should have the following format:
|
|
68
|
+
|
|
69
|
+
```ini
|
|
70
|
+
# lines starting with hash (#) are comments
|
|
71
|
+
# empty lines, like the following, are ignored
|
|
72
|
+
|
|
73
|
+
# conventional commit "type" mappings are
|
|
74
|
+
# configured in a [types] section. the following
|
|
75
|
+
# mirrors the default configuration:
|
|
76
|
+
[types]
|
|
77
|
+
.*!=major
|
|
78
|
+
feat.*=minor
|
|
79
|
+
.*=patch
|
|
80
|
+
|
|
81
|
+
# conventional commit "footer" mappings are
|
|
82
|
+
# configured in a [footers] section. the following
|
|
83
|
+
# mirrors the default configuration:
|
|
84
|
+
[footers]
|
|
85
|
+
BREAKING[\-\.]CHANGE=major
|
|
86
|
+
|
|
87
|
+
# in each of the above sections, each line
|
|
88
|
+
# represents a key-value pair. the key is a regex
|
|
89
|
+
# and the value is a component type to be
|
|
90
|
+
# incremented if the regex is a match.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
There is a sample configuration file located in this repo as `config/conventional-semver.conf` which contains additional comments and explanations, you can customize it to fit your needs and then place it at one of the well-known locations mentioned above.
|
|
94
|
+
|
|
95
|
+
## Not yet Implemented
|
|
96
|
+
|
|
97
|
+
### Generating CHANGELOG
|
|
98
|
+
|
|
99
|
+
To generate a CHANGELOG you specify the `--changelog [filename]` switch, this takes an optional filename argument. If no filename is provided a default filename of `./CHANGELOG` is used to emit a file into the current working directory.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
% conventional-semver --changelog
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### CHANGELOG Templates
|
|
108
|
+
|
|
109
|
+
The CHANGELOG output is controlled through one or more templates. This includes a required "entry" template, and optional "header" and "footer" templates. Combined this is meant to provide enough flexibility that you could emit templates in various structured formats such as XML, JSON, Markdown, etc.
|
|
110
|
+
|
|
111
|
+
The default templates are meant to produce a generic TEXT file that is markdown-friendly.
|
|
112
|
+
|
|
113
|
+
##### CHANGELOG Entry Template
|
|
114
|
+
|
|
115
|
+
For each SEMVER increment a CHANGELOG Entry is emitted.
|
|
116
|
+
|
|
117
|
+
The format of each CHANGELOG Entry is taken from a file named `changelog-entry.template`, when `--changelog` is specified this file is required to be present or the command will fail.
|
|
118
|
+
|
|
119
|
+
This is the default content of this template:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
$(DATE) $(SEMVER)-$(HASH)
|
|
123
|
+
$(TYPE)$(SCOPE): $(MESSAGE)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The entry template supports the following expando variables:
|
|
127
|
+
|
|
128
|
+
| Expando | Comment |
|
|
129
|
+
|-|-|
|
|
130
|
+
| $(DATE) | The date of the commit which caused SEMVER increment. This is emitted in the default culture of the current environment. |
|
|
131
|
+
| $(SEMVER) | The calculated SEMVER value. |
|
|
132
|
+
| $(HASH) | The short-form git commit hash. |
|
|
133
|
+
| $(TYPE) | The Conventional Commits `<type>` value. |
|
|
134
|
+
| $(SCOPE) | If any, the Conventional Commits `[scope]` value, including parenthesis. |
|
|
135
|
+
| $(MESSAGE) | The commit message, sans `<type>` and `[scope]`. |
|
|
136
|
+
| $(BODY) | If any, the commit body. |
|
|
137
|
+
| $(TRAILERS) | If any, the commit trailers (footers). |
|
|
138
|
+
|
|
139
|
+
##### CHANGELOG Header Template
|
|
140
|
+
|
|
141
|
+
The header prepended to the CHANGELOG comes from a file named `changelog-header.template`.
|
|
142
|
+
|
|
143
|
+
There are no special expando variables supported in the header.
|
|
144
|
+
|
|
145
|
+
##### CHANGELOG Footer Template
|
|
146
|
+
|
|
147
|
+
The footer appended to the CHANGELOG comes from a file named `changelog-footer.template`.
|
|
148
|
+
|
|
149
|
+
There are no special expando variables supported in the footer.
|
|
150
|
+
|
|
151
|
+
##### CHANGELOG Template Caveats
|
|
152
|
+
|
|
153
|
+
No escaping is performed on any of the emitted values. Thus, it is possible for commit messages to interact with parsers/renderers in unintended ways. For example if you make a template to emit an HTML changelog, and a commit message contains content which looks like an 'element', it will most likely result in a changelog that doesn't render as expected.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "py-conventional-semver"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "..a Python fork of sw4k's `conventional-semver` tool."
|
|
5
|
+
keywords = ["SEMVER", "conventional-semver", "conventional-commits", "sw4k"]
|
|
6
|
+
authors = [
|
|
7
|
+
{ name="Shaun Wilson", email="mrshaunwilson@msn.com" }
|
|
8
|
+
]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python"
|
|
15
|
+
]
|
|
16
|
+
requires-python = ">=3.11"
|
|
17
|
+
dependencies = [
|
|
18
|
+
"hanaro (>=1.0.0)"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"coverage",
|
|
24
|
+
"mypy",
|
|
25
|
+
"punit (>=1.4.5)",
|
|
26
|
+
"twine"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Documentation = "https://conventional-semver.readthedocs.io/"
|
|
31
|
+
Homepage = "https://github.com/wilson0x4d/conventional-semver"
|
|
32
|
+
Repository = "https://github.com/wilson0x4d/conventional-semver.git"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.data-files]
|
|
35
|
+
bin = [".scripts/conventional-semver"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
"py-conventional-semver" = ["py.typed"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2024 sw4k
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import hanaro
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from .Configuration import Configuration
|
|
11
|
+
from .GitEntry import GitEntry
|
|
12
|
+
from .OutputGenerator import OutputGenerator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChangelogOutputGenerator(OutputGenerator):
|
|
16
|
+
|
|
17
|
+
__config: Configuration
|
|
18
|
+
__logger: logging.Logger
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Configuration) -> None:
|
|
21
|
+
self.__config = config
|
|
22
|
+
self.__logger = hanaro.get_logger()
|
|
23
|
+
|
|
24
|
+
def handle_commit_entry(self, entry: GitEntry) -> None:
|
|
25
|
+
# TODO: implement handling logic
|
|
26
|
+
self.__logger.warn('TODO: HandleCommitEntry')
|
|
27
|
+
|
|
28
|
+
def generate_output(self) -> None:
|
|
29
|
+
# TODO: implement output generation
|
|
30
|
+
self.__logger.warn('TODO: GenerateOutput')
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2024 sw4k
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import hanaro
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from .Configuration import Configuration
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandlineProcessor:
|
|
15
|
+
|
|
16
|
+
__config: Configuration
|
|
17
|
+
__logger: logging.Logger
|
|
18
|
+
__paths: list[str]
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Configuration, version: str, commit: str) -> None:
|
|
21
|
+
self.__commit = commit
|
|
22
|
+
self.__config = config
|
|
23
|
+
self.__logger = hanaro.get_logger()
|
|
24
|
+
self.__paths = []
|
|
25
|
+
self.__version = version
|
|
26
|
+
|
|
27
|
+
def __print_info(self) -> None:
|
|
28
|
+
print(f'conventional-semver {self.__version} ({self.__commit})')
|
|
29
|
+
|
|
30
|
+
def __print_usage(self) -> None:
|
|
31
|
+
print()
|
|
32
|
+
print('Usage:')
|
|
33
|
+
print('\conventional-semver [options] [repo-path]')
|
|
34
|
+
print()
|
|
35
|
+
print('Options:')
|
|
36
|
+
print('\t--help print usage, then exit.')
|
|
37
|
+
print('\t--version print version info, then exit.')
|
|
38
|
+
print('\t--verbose enable verbose (debug) output.')
|
|
39
|
+
print('\t--commit <hash> indicates which commit hash to start changelog from.')
|
|
40
|
+
print('\t--tag <name> indicates which tag to start changelog from.')
|
|
41
|
+
print('\t--changelog <file> overrides the name of the file the changelog is written to, otherwise \'changelog.md\' is the default.')
|
|
42
|
+
print('\t--no-semver disable SEMVER output to STDOUT.')
|
|
43
|
+
print('\t--major SEMVER \'Major\' component will start with this value, default is \'0\'.')
|
|
44
|
+
print('\t--minor SEMVER \'Minor\' component will start with this value, default is \'0\'.')
|
|
45
|
+
print('\t--patch SEMVER \'Patch\' component will start with this value, default is \'0\'.')
|
|
46
|
+
print('\t--git-path <path> overrides the path to `git` tool, otherwise `git` must be in environment PATH.')
|
|
47
|
+
print()
|
|
48
|
+
print('Parameters:')
|
|
49
|
+
print('\trepo-path the path of the git repository to process, if not specified defaults to working directory.')
|
|
50
|
+
print()
|
|
51
|
+
|
|
52
|
+
def process_command_line(self, argv: list[str] | None = None) -> None:
|
|
53
|
+
if argv is None:
|
|
54
|
+
argv = sys.argv
|
|
55
|
+
args = argv[1:]
|
|
56
|
+
i = 0
|
|
57
|
+
while i < len(args):
|
|
58
|
+
arg = args[i]
|
|
59
|
+
if arg == '--help':
|
|
60
|
+
self.__print_info()
|
|
61
|
+
self.__print_usage()
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
elif arg == '--version':
|
|
64
|
+
self.__print_info()
|
|
65
|
+
sys.exit(0)
|
|
66
|
+
elif arg == '--no-semver':
|
|
67
|
+
self.__config.disable_semver_output = True
|
|
68
|
+
elif arg == '--changelog':
|
|
69
|
+
i += 1
|
|
70
|
+
if i >= len(args) or args[i].startswith('-'):
|
|
71
|
+
raise IndexError('missing argument to `' + arg + '` option')
|
|
72
|
+
self.__config.changelog_output_file = args[i]
|
|
73
|
+
elif arg == '--commit':
|
|
74
|
+
if getattr(self.__config, 'start_tag', ''):
|
|
75
|
+
raise RuntimeError('cannot specify both `--commit` and `--tag` options.')
|
|
76
|
+
i += 1
|
|
77
|
+
if i >= len(args) or args[i].startswith('-'):
|
|
78
|
+
raise IndexError('missing argument to `' + arg + '` option')
|
|
79
|
+
self.__config.start_commit_hash = args[i]
|
|
80
|
+
elif arg == '--config':
|
|
81
|
+
i += 1
|
|
82
|
+
if i >= len(args) or args[i].startswith('-'):
|
|
83
|
+
raise IndexError('missing argument to `' + arg + '` option')
|
|
84
|
+
self.__config.config_file = args[i]
|
|
85
|
+
elif arg == '--git-path':
|
|
86
|
+
i += 1
|
|
87
|
+
if i >= len(args) or args[i].startswith('-'):
|
|
88
|
+
raise IndexError('missing argument to `' + arg + '` option')
|
|
89
|
+
self.__config.git_path = args[i]
|
|
90
|
+
elif arg == '--major':
|
|
91
|
+
i += 1
|
|
92
|
+
if i >= len(args) or args[i].startswith('-'):
|
|
93
|
+
raise IndexError('missing argument to `' + arg + '` option')
|
|
94
|
+
self.__config.major_start = int(args[i])
|
|
95
|
+
elif arg == '--minor':
|
|
96
|
+
i += 1
|
|
97
|
+
if i >= len(args) or args[i].startswith('-'):
|
|
98
|
+
raise IndexError('missing argument to `' + arg + '` option')
|
|
99
|
+
self.__config.minor_start = int(args[i])
|
|
100
|
+
elif arg == '--patch':
|
|
101
|
+
i += 1
|
|
102
|
+
if i >= len(args) or args[i].startswith('-'):
|
|
103
|
+
raise IndexError('missing argument to `' + arg + '` option')
|
|
104
|
+
self.__config.patch_start = int(args[i])
|
|
105
|
+
elif arg == '--tag':
|
|
106
|
+
if getattr(self.__config, 'start_commit_hash', ''):
|
|
107
|
+
raise RuntimeError('cannot specify both `--commit` and `--tag` options.')
|
|
108
|
+
i += 1
|
|
109
|
+
if i >= len(args) or args[i].startswith('-'):
|
|
110
|
+
raise IndexError('missing argument to `' + arg + '` option')
|
|
111
|
+
self.__config.start_tag = args[i]
|
|
112
|
+
elif arg == '--verbose':
|
|
113
|
+
# TODO
|
|
114
|
+
pass
|
|
115
|
+
elif not self.__config.repo_path and not arg.startswith('-'):
|
|
116
|
+
self.__config.repo_path = arg
|
|
117
|
+
else:
|
|
118
|
+
self.__print_usage()
|
|
119
|
+
raise ValueError('unexpected argument: ' + arg)
|
|
120
|
+
i += 1
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2024 sw4k
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import hanaro
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from .SemverComponentType import SemverComponentType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Configuration:
|
|
17
|
+
|
|
18
|
+
changelog_output_file: str
|
|
19
|
+
config_file: str
|
|
20
|
+
disable_semver_output: bool
|
|
21
|
+
footers: list[tuple[re.Pattern, SemverComponentType]]
|
|
22
|
+
git_path: str
|
|
23
|
+
major_start: int
|
|
24
|
+
minor_start: int
|
|
25
|
+
patch_start: int
|
|
26
|
+
start_commit_hash: str
|
|
27
|
+
start_tag: str
|
|
28
|
+
types: list[tuple[re.Pattern, SemverComponentType]]
|
|
29
|
+
repo_path: str
|
|
30
|
+
|
|
31
|
+
__logger: logging.Logger
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self.__logger = hanaro.get_logger()
|
|
35
|
+
|
|
36
|
+
self.changelog_output_file = ''
|
|
37
|
+
self.config_file = ''
|
|
38
|
+
self.disable_semver_output = False
|
|
39
|
+
self.footers = []
|
|
40
|
+
self.git_path = ''
|
|
41
|
+
self.major_start = 0
|
|
42
|
+
self.minor_start = 0
|
|
43
|
+
self.patch_start = 0
|
|
44
|
+
self.start_commit_hash = ''
|
|
45
|
+
self.start_tag = ''
|
|
46
|
+
self.types = []
|
|
47
|
+
self.repo_path = ''
|
|
48
|
+
|
|
49
|
+
def __build_footer_regex(self, input_str: str) -> re.Pattern:
|
|
50
|
+
pattern = r'[\S\s]*' + input_str + r'[\S\s]*'
|
|
51
|
+
return re.compile(pattern, re.MULTILINE | re.IGNORECASE)
|
|
52
|
+
|
|
53
|
+
def __build_type_regex(self, input_str: str) -> re.Pattern:
|
|
54
|
+
pattern = r'^' + input_str + r':[\S\s]*'
|
|
55
|
+
return re.compile(pattern, re.MULTILINE | re.IGNORECASE)
|
|
56
|
+
|
|
57
|
+
def __create_entry(self, entry: str, entry_code: str) -> bool:
|
|
58
|
+
idx = entry.find('=')
|
|
59
|
+
if idx == -1:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
regex = self.__build_type_regex(entry[:idx].strip())
|
|
63
|
+
|
|
64
|
+
value_part = entry[idx + 1 :].strip()
|
|
65
|
+
if not value_part:
|
|
66
|
+
return False
|
|
67
|
+
component_code = value_part[0].lower()
|
|
68
|
+
|
|
69
|
+
component_type = None
|
|
70
|
+
if component_code == 'j':
|
|
71
|
+
component_type = SemverComponentType.MAJOR
|
|
72
|
+
elif component_code == 'n':
|
|
73
|
+
component_type = SemverComponentType.MINOR
|
|
74
|
+
elif component_code == 't':
|
|
75
|
+
component_type = SemverComponentType.PATCH
|
|
76
|
+
else:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
if entry_code == 't':
|
|
80
|
+
self.types.append((regex, component_type))
|
|
81
|
+
elif entry_code == 'f':
|
|
82
|
+
self.footers.append((regex, component_type))
|
|
83
|
+
else:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
def __process_configuration_file(self, path: str) -> bool:
|
|
89
|
+
config_path = Path(path).expanduser()
|
|
90
|
+
if not config_path.is_file():
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
entry_type = '?' # tracks whether we are inside [types] or [footers]
|
|
94
|
+
|
|
95
|
+
with config_path.open('r', encoding='utf-8') as stream:
|
|
96
|
+
for raw_line in stream:
|
|
97
|
+
line = raw_line.rstrip('\n')
|
|
98
|
+
if not line or line.startswith('#'):
|
|
99
|
+
continue
|
|
100
|
+
if line == '[types]':
|
|
101
|
+
entry_type = 't'
|
|
102
|
+
elif line == '[footers]':
|
|
103
|
+
entry_type = 'f'
|
|
104
|
+
else:
|
|
105
|
+
if not self.__create_entry(line, entry_type):
|
|
106
|
+
self.__logger.warn('Could not configure entry from: ' + line)
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def __print_configuration_summary(self) -> None:
|
|
110
|
+
# TODO:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def process_configuration(self) -> None:
|
|
114
|
+
config_processed = False
|
|
115
|
+
|
|
116
|
+
if self.config_file:
|
|
117
|
+
config_processed = self.__process_configuration_file(self.config_file)
|
|
118
|
+
if not config_processed:
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
f'Failed to process expected configuration file: {self.config_file}'
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
candidate_paths = [
|
|
124
|
+
'./conventional-semver.json',
|
|
125
|
+
os.path.expanduser('~/.config/conventional-semver/settings.json'),
|
|
126
|
+
'/etc/conventional-semver/settings.json',
|
|
127
|
+
]
|
|
128
|
+
for cand in candidate_paths:
|
|
129
|
+
if self.__process_configuration_file(cand):
|
|
130
|
+
config_processed = True
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
if not config_processed:
|
|
134
|
+
# apply a default set of regexes
|
|
135
|
+
self.types.append(
|
|
136
|
+
(self.__build_type_regex('.*!'), SemverComponentType.MAJOR)
|
|
137
|
+
)
|
|
138
|
+
self.types.append(
|
|
139
|
+
(self.__build_type_regex('feat.*'), SemverComponentType.MINOR)
|
|
140
|
+
)
|
|
141
|
+
self.types.append(
|
|
142
|
+
(self.__build_type_regex('.*'), SemverComponentType.PATCH)
|
|
143
|
+
)
|
|
144
|
+
self.footers.append(
|
|
145
|
+
(
|
|
146
|
+
self.__build_footer_regex(r'BREAKING[\-\.]CHANGE'),
|
|
147
|
+
SemverComponentType.MAJOR,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if not self.repo_path:
|
|
152
|
+
self.repo_path = str(Path.cwd())
|
|
153
|
+
|
|
154
|
+
if (self.start_commit_hash or self.start_tag) and not self.changelog_output_file:
|
|
155
|
+
self.changelog_output_file = str(Path.cwd() / 'changelog.md')
|
|
156
|
+
|
|
157
|
+
if not self.git_path:
|
|
158
|
+
self.git_path = 'git'
|
|
159
|
+
|
|
160
|
+
self.__print_configuration_summary()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# git_adapter.py (only the relevant parts are shown)
|
|
2
|
+
|
|
3
|
+
import hanaro
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .Configuration import Configuration
|
|
10
|
+
from .GitEntryParser import GitEntryParser
|
|
11
|
+
from .GitLogStream import GitLogStream
|
|
12
|
+
from .OutputGenerator import OutputGenerator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GitAdapter:
|
|
16
|
+
|
|
17
|
+
__config: Configuration
|
|
18
|
+
__git_log_stream: GitLogStream
|
|
19
|
+
__logger: logging.Logger
|
|
20
|
+
__output_generators: list[OutputGenerator]
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
config: Configuration,
|
|
25
|
+
git_entry_parser: GitEntryParser,
|
|
26
|
+
output_generators: list[OutputGenerator],
|
|
27
|
+
) -> None:
|
|
28
|
+
self.__config = config
|
|
29
|
+
self.__logger = hanaro.get_logger()
|
|
30
|
+
self.__git_log_stream = GitLogStream(git_entry_parser)
|
|
31
|
+
self.__output_generators = list(output_generators)
|
|
32
|
+
|
|
33
|
+
def process_git_log(self) -> None:
|
|
34
|
+
argv = [
|
|
35
|
+
self.__config.git_path,
|
|
36
|
+
'--no-pager',
|
|
37
|
+
'-C',
|
|
38
|
+
self.__config.repo_path,
|
|
39
|
+
'log',
|
|
40
|
+
'--reverse',
|
|
41
|
+
'--format=tformat:%H%n%D%n%B%n%xef',
|
|
42
|
+
]
|
|
43
|
+
try:
|
|
44
|
+
proc = subprocess.Popen(
|
|
45
|
+
argv,
|
|
46
|
+
stdout=subprocess.PIPE,
|
|
47
|
+
stderr=subprocess.PIPE,
|
|
48
|
+
cwd=self.__config.repo_path,
|
|
49
|
+
)
|
|
50
|
+
except OSError as ex:
|
|
51
|
+
raise RuntimeError(f'Failed to start git: {ex}') from ex
|
|
52
|
+
|
|
53
|
+
stdout_bytes, stderr_bytes = proc.communicate()
|
|
54
|
+
|
|
55
|
+
if stderr_bytes:
|
|
56
|
+
os.write(sys.stderr.fileno(), stderr_bytes)
|
|
57
|
+
|
|
58
|
+
self.__git_log_stream.write(stdout_bytes, len(stdout_bytes))
|
|
59
|
+
|
|
60
|
+
entry = self.__git_log_stream.readentry()
|
|
61
|
+
while not entry.is_empty():
|
|
62
|
+
for generator in self.__output_generators:
|
|
63
|
+
generator.handle_commit_entry(entry)
|
|
64
|
+
entry = self.__git_log_stream.readentry()
|
|
65
|
+
|
|
66
|
+
if proc.returncode != 0:
|
|
67
|
+
self.__logger.error(f'git process exited with non-zero code {proc.returncode}')
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2024 sw4k
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GitEntry:
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
commit_hash: str = '',
|
|
14
|
+
body: str = '',
|
|
15
|
+
footers: Optional[list[str]] = None,
|
|
16
|
+
refs: Optional[list[str]] = None,
|
|
17
|
+
subject: str = '',
|
|
18
|
+
) -> None:
|
|
19
|
+
self.commit_hash = commit_hash
|
|
20
|
+
self.body = body
|
|
21
|
+
self.footers = footers if footers is not None else []
|
|
22
|
+
self.refs = refs if refs is not None else []
|
|
23
|
+
self.subject = subject
|
|
24
|
+
|
|
25
|
+
def is_empty(self) -> bool:
|
|
26
|
+
"""Return ``True`` when the entry contains no data."""
|
|
27
|
+
return (
|
|
28
|
+
not self.commit_hash
|
|
29
|
+
and not self.body
|
|
30
|
+
and not self.footers
|
|
31
|
+
and not self.refs
|
|
32
|
+
and not self.subject
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
"""Produce a string representation of the git entry."""
|
|
37
|
+
if self.is_empty():
|
|
38
|
+
return ''
|
|
39
|
+
|
|
40
|
+
result = self.commit_hash + '\n'
|
|
41
|
+
|
|
42
|
+
if self.refs:
|
|
43
|
+
result += ', '.join(self.refs)
|
|
44
|
+
|
|
45
|
+
result += '\n' + self.subject + '\n\n' + self.body + '\n'
|
|
46
|
+
|
|
47
|
+
if self.footers:
|
|
48
|
+
result += ','.join(self.footers)
|
|
49
|
+
|
|
50
|
+
return result
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2024 sw4k
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .GitEntry import GitEntry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GitEntryParser:
|
|
11
|
+
|
|
12
|
+
def parse(self, input_str: str) -> GitEntry:
|
|
13
|
+
if not input_str:
|
|
14
|
+
return GitEntry()
|
|
15
|
+
|
|
16
|
+
entry = GitEntry()
|
|
17
|
+
|
|
18
|
+
lines = input_str.split('\n')
|
|
19
|
+
|
|
20
|
+
entry.commit_hash = lines[0]
|
|
21
|
+
|
|
22
|
+
refs_raw = lines[1]
|
|
23
|
+
if refs_raw:
|
|
24
|
+
entry.refs = [
|
|
25
|
+
ref.strip()
|
|
26
|
+
for ref in refs_raw.split(',')
|
|
27
|
+
if ref
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
subject_end = lines.index('', 2)
|
|
32
|
+
except ValueError:
|
|
33
|
+
subject_end = len(lines)
|
|
34
|
+
|
|
35
|
+
subject_lines = lines[2:subject_end]
|
|
36
|
+
entry.subject = '\n'.join(subject_lines)
|
|
37
|
+
|
|
38
|
+
body_start = subject_end + 1
|
|
39
|
+
if body_start < len(lines):
|
|
40
|
+
try:
|
|
41
|
+
body_end = lines.index('', body_start)
|
|
42
|
+
except ValueError:
|
|
43
|
+
body_end = len(lines)
|
|
44
|
+
|
|
45
|
+
body_lines = lines[body_start:body_end]
|
|
46
|
+
entry.body = '\n'.join(body_lines)
|
|
47
|
+
|
|
48
|
+
footer_start = body_end + 1
|
|
49
|
+
if footer_start < len(lines):
|
|
50
|
+
entry.footers = [
|
|
51
|
+
footer
|
|
52
|
+
for footer in lines[footer_start:]
|
|
53
|
+
if footer
|
|
54
|
+
]
|
|
55
|
+
else:
|
|
56
|
+
entry.footers = []
|
|
57
|
+
|
|
58
|
+
return entry
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2024 sw4k
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .GitEntry import GitEntry
|
|
8
|
+
from .GitEntryParser import GitEntryParser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GitLogStream:
|
|
12
|
+
|
|
13
|
+
__git_entry_parser: GitEntryParser
|
|
14
|
+
__buffer: bytearray
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
git_entry_parser: GitEntryParser
|
|
19
|
+
) -> None:
|
|
20
|
+
self.__buffer = bytearray()
|
|
21
|
+
self.__git_entry_parser = git_entry_parser
|
|
22
|
+
|
|
23
|
+
def write(self, buf: bytes, count: int) -> None:
|
|
24
|
+
self.__buffer.extend(buf[:count])
|
|
25
|
+
|
|
26
|
+
def readentry(self) -> GitEntry:
|
|
27
|
+
terminator = 0xEF
|
|
28
|
+
try:
|
|
29
|
+
term_index = self.__buffer.index(terminator)
|
|
30
|
+
except ValueError:
|
|
31
|
+
return GitEntry()
|
|
32
|
+
|
|
33
|
+
entry_bytes = self.__buffer[:term_index]
|
|
34
|
+
del self.__buffer[: term_index + 2]
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
entry_str = entry_bytes.decode('utf-8')
|
|
38
|
+
except UnicodeDecodeError:
|
|
39
|
+
entry_str = entry_bytes.decode('utf-8', errors='replace')
|
|
40
|
+
return self.__git_entry_parser.parse(entry_str.strip())
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2024 sw4k
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from .GitEntry import GitEntry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class OutputGenerator(Protocol):
|
|
13
|
+
|
|
14
|
+
def handle_commit_entry(self, entry: GitEntry) -> None:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
def generate_output(self) -> None:
|
|
18
|
+
...
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2024 sw4k
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .Configuration import Configuration
|
|
8
|
+
from .GitEntry import GitEntry
|
|
9
|
+
from .OutputGenerator import OutputGenerator
|
|
10
|
+
from .SemverComponentType import SemverComponentType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SemverOutputGenerator(OutputGenerator):
|
|
14
|
+
|
|
15
|
+
__config: Configuration
|
|
16
|
+
__major: int
|
|
17
|
+
__minor: int
|
|
18
|
+
__patch: int
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Configuration) -> None:
|
|
21
|
+
self.__config = config
|
|
22
|
+
self.__major = config.major_start
|
|
23
|
+
self.__minor = config.minor_start
|
|
24
|
+
self.__patch = config.patch_start
|
|
25
|
+
|
|
26
|
+
def handle_commit_entry(self, entry: GitEntry) -> None:
|
|
27
|
+
if entry.is_empty():
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
semver_component = SemverComponentType.NONE
|
|
31
|
+
|
|
32
|
+
for regex, comp in self.__config.types:
|
|
33
|
+
if comp.value > semver_component.value:
|
|
34
|
+
if regex.search(entry.subject):
|
|
35
|
+
semver_component = comp
|
|
36
|
+
|
|
37
|
+
for regex, comp in self.__config.footers:
|
|
38
|
+
if comp.value > semver_component.value:
|
|
39
|
+
for footer in entry.footers:
|
|
40
|
+
if regex.search(footer):
|
|
41
|
+
semver_component = comp
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
if semver_component == SemverComponentType.MAJOR:
|
|
45
|
+
self.__major += 1
|
|
46
|
+
self.__minor = 0
|
|
47
|
+
self.__patch = 0
|
|
48
|
+
elif semver_component == SemverComponentType.MINOR:
|
|
49
|
+
self.__minor += 1
|
|
50
|
+
self.__patch = 0
|
|
51
|
+
elif semver_component == SemverComponentType.PATCH:
|
|
52
|
+
self.__patch += 1
|
|
53
|
+
|
|
54
|
+
def generate_output(self) -> None:
|
|
55
|
+
print(f'{self.__major}.{self.__minor}.{self.__patch}')
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from .ChangelogOutputGenerator import ChangelogOutputGenerator
|
|
5
|
+
from .CommandLineProcessor import CommandlineProcessor
|
|
6
|
+
from .Configuration import Configuration
|
|
7
|
+
from .GitAdapter import GitAdapter
|
|
8
|
+
from .GitEntry import GitEntry
|
|
9
|
+
from .GitEntryParser import GitEntryParser
|
|
10
|
+
from .GitLogStream import GitLogStream
|
|
11
|
+
from .OutputGenerator import OutputGenerator
|
|
12
|
+
from .SemverOutputGenerator import SemverOutputGenerator
|
|
13
|
+
from .SemverComponentType import SemverComponentType
|
|
14
|
+
|
|
15
|
+
__version__ = '1.0.0'
|
|
16
|
+
__commit__ = '11d7129'
|
|
17
|
+
__all__ = [
|
|
18
|
+
'__version__', '__commit__',
|
|
19
|
+
'ChangelogOutputGenerator',
|
|
20
|
+
'CommandlineProcessor',
|
|
21
|
+
'Configuration',
|
|
22
|
+
'GitAdapter',
|
|
23
|
+
'GitEntry',
|
|
24
|
+
'GitEntryParser',
|
|
25
|
+
'GitLogStream',
|
|
26
|
+
'OutputGenerator',
|
|
27
|
+
'SemverOutputGenerator',
|
|
28
|
+
'SemverComponentType',
|
|
29
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Shaun Wilson
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2024 sw4k
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .Configuration import Configuration
|
|
9
|
+
from .CommandLineProcessor import CommandlineProcessor
|
|
10
|
+
from .ChangelogOutputGenerator import ChangelogOutputGenerator
|
|
11
|
+
from .GitEntryParser import GitEntryParser
|
|
12
|
+
from .GitAdapter import GitAdapter
|
|
13
|
+
from .OutputGenerator import OutputGenerator
|
|
14
|
+
from .SemverOutputGenerator import SemverOutputGenerator
|
|
15
|
+
from . import __version__, __commit__
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv: list[str]) -> int:
|
|
19
|
+
config = Configuration()
|
|
20
|
+
cmd_processor = CommandlineProcessor(config, __version__, __commit__)
|
|
21
|
+
cmd_processor.process_command_line(argv)
|
|
22
|
+
config.process_configuration()
|
|
23
|
+
output_generators = list[OutputGenerator]()
|
|
24
|
+
if not config.disable_semver_output:
|
|
25
|
+
output_generators.append(SemverOutputGenerator(config))
|
|
26
|
+
if config.changelog_output_file:
|
|
27
|
+
output_generators.append(ChangelogOutputGenerator(config))
|
|
28
|
+
git_entry_parser = GitEntryParser()
|
|
29
|
+
git_adapter = GitAdapter(
|
|
30
|
+
config,
|
|
31
|
+
git_entry_parser,
|
|
32
|
+
output_generators,
|
|
33
|
+
)
|
|
34
|
+
git_adapter.process_git_log()
|
|
35
|
+
for generator in output_generators:
|
|
36
|
+
generator.generate_output()
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == '__main__':
|
|
41
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-conventional-semver
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ..a Python fork of sw4k's `conventional-semver` tool.
|
|
5
|
+
Author-email: Shaun Wilson <mrshaunwilson@msn.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://conventional-semver.readthedocs.io/
|
|
8
|
+
Project-URL: Homepage, https://github.com/wilson0x4d/conventional-semver
|
|
9
|
+
Project-URL: Repository, https://github.com/wilson0x4d/conventional-semver.git
|
|
10
|
+
Keywords: SEMVER,conventional-semver,conventional-commits,sw4k
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: hanaro>=1.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: coverage; extra == "dev"
|
|
20
|
+
Requires-Dist: mypy; extra == "dev"
|
|
21
|
+
Requires-Dist: punit>=1.4.5; extra == "dev"
|
|
22
|
+
Requires-Dist: twine; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
**conventional-semver** is a [Conventional Commits](https://www.conventionalcommits.org/) processor designed to emit a [SEMVER](https://semver.org) as part of a build pipeline.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
Usage:
|
|
32
|
+
conventional-semver [options] [repo-path]
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--help print usage, then exit.
|
|
36
|
+
--version print version info, then exit.
|
|
37
|
+
--verbose enable verbose (debug) output.
|
|
38
|
+
--commit <hash> indicates which commit hash to start changelog from.
|
|
39
|
+
--tag <name> indicates which tag to start changelog from.
|
|
40
|
+
--changelog <file> overrides the name of the file the changelog is written to, otherwise 'changelog.md' is the default.
|
|
41
|
+
--no-semver disable SEMVER output to STDOUT.
|
|
42
|
+
--major SEMVER 'Major' component will start with this value, default is '0'.
|
|
43
|
+
--minor SEMVER 'Minor' component will start with this value, default is '0'.
|
|
44
|
+
--patch SEMVER 'Patch' component will start with this value, default is '0'.
|
|
45
|
+
--git-path <path> overrides the path to `git` tool, otherwise `git` must be in environment PATH.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
repo-path the path of the git repository to process, if not specified defaults to working directory.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Generating SEMVER
|
|
52
|
+
|
|
53
|
+
When you run **conventional-semver** from within a `git` repository, it will automatically process the log and emit a semver.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
% conventional-semver
|
|
59
|
+
0.1.23
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This allows you to pull a semver into an Environment Variable, evaluate it as an Argument to another tool, or pipe it to a file/stream for additional processing:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
% export SEMVER=$(conventional-semver)
|
|
66
|
+
% echo $SEMVER
|
|
67
|
+
0.1.23
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Override Baseline SEMVER
|
|
71
|
+
|
|
72
|
+
Projects adopting Conventional Commits may need to customize the baseline SEMVER, rather than starting from `0.0.0`. This can be done with the `--major`, `--minor`, and `--patch` arguments.
|
|
73
|
+
|
|
74
|
+
When run on a repo containing three non-conventional commits:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
% conventional-semver --major 1 --minor 2 --patch 3
|
|
78
|
+
1.2.6
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### SEMVER Configuration
|
|
82
|
+
|
|
83
|
+
When run without any command-line arguments a default set of settings are used which implement a standard Conventional Commits behavior.
|
|
84
|
+
|
|
85
|
+
To customize behavior a configuration file may be created. This file can be passed in using a `--config` argument, or, placed into one of the following well-known locations (and in the following order):
|
|
86
|
+
|
|
87
|
+
* `./conventional-semver.conf` (working directory.)
|
|
88
|
+
* `~/.config/conventional-semver/settings.conf` (user profile `.config` directory.)
|
|
89
|
+
* `/etc/conventional-semver/settings.conf` (root `/etc` directory.)
|
|
90
|
+
|
|
91
|
+
The configuration file should have the following format:
|
|
92
|
+
|
|
93
|
+
```ini
|
|
94
|
+
# lines starting with hash (#) are comments
|
|
95
|
+
# empty lines, like the following, are ignored
|
|
96
|
+
|
|
97
|
+
# conventional commit "type" mappings are
|
|
98
|
+
# configured in a [types] section. the following
|
|
99
|
+
# mirrors the default configuration:
|
|
100
|
+
[types]
|
|
101
|
+
.*!=major
|
|
102
|
+
feat.*=minor
|
|
103
|
+
.*=patch
|
|
104
|
+
|
|
105
|
+
# conventional commit "footer" mappings are
|
|
106
|
+
# configured in a [footers] section. the following
|
|
107
|
+
# mirrors the default configuration:
|
|
108
|
+
[footers]
|
|
109
|
+
BREAKING[\-\.]CHANGE=major
|
|
110
|
+
|
|
111
|
+
# in each of the above sections, each line
|
|
112
|
+
# represents a key-value pair. the key is a regex
|
|
113
|
+
# and the value is a component type to be
|
|
114
|
+
# incremented if the regex is a match.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
There is a sample configuration file located in this repo as `config/conventional-semver.conf` which contains additional comments and explanations, you can customize it to fit your needs and then place it at one of the well-known locations mentioned above.
|
|
118
|
+
|
|
119
|
+
## Not yet Implemented
|
|
120
|
+
|
|
121
|
+
### Generating CHANGELOG
|
|
122
|
+
|
|
123
|
+
To generate a CHANGELOG you specify the `--changelog [filename]` switch, this takes an optional filename argument. If no filename is provided a default filename of `./CHANGELOG` is used to emit a file into the current working directory.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
% conventional-semver --changelog
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### CHANGELOG Templates
|
|
132
|
+
|
|
133
|
+
The CHANGELOG output is controlled through one or more templates. This includes a required "entry" template, and optional "header" and "footer" templates. Combined this is meant to provide enough flexibility that you could emit templates in various structured formats such as XML, JSON, Markdown, etc.
|
|
134
|
+
|
|
135
|
+
The default templates are meant to produce a generic TEXT file that is markdown-friendly.
|
|
136
|
+
|
|
137
|
+
##### CHANGELOG Entry Template
|
|
138
|
+
|
|
139
|
+
For each SEMVER increment a CHANGELOG Entry is emitted.
|
|
140
|
+
|
|
141
|
+
The format of each CHANGELOG Entry is taken from a file named `changelog-entry.template`, when `--changelog` is specified this file is required to be present or the command will fail.
|
|
142
|
+
|
|
143
|
+
This is the default content of this template:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
$(DATE) $(SEMVER)-$(HASH)
|
|
147
|
+
$(TYPE)$(SCOPE): $(MESSAGE)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The entry template supports the following expando variables:
|
|
151
|
+
|
|
152
|
+
| Expando | Comment |
|
|
153
|
+
|-|-|
|
|
154
|
+
| $(DATE) | The date of the commit which caused SEMVER increment. This is emitted in the default culture of the current environment. |
|
|
155
|
+
| $(SEMVER) | The calculated SEMVER value. |
|
|
156
|
+
| $(HASH) | The short-form git commit hash. |
|
|
157
|
+
| $(TYPE) | The Conventional Commits `<type>` value. |
|
|
158
|
+
| $(SCOPE) | If any, the Conventional Commits `[scope]` value, including parenthesis. |
|
|
159
|
+
| $(MESSAGE) | The commit message, sans `<type>` and `[scope]`. |
|
|
160
|
+
| $(BODY) | If any, the commit body. |
|
|
161
|
+
| $(TRAILERS) | If any, the commit trailers (footers). |
|
|
162
|
+
|
|
163
|
+
##### CHANGELOG Header Template
|
|
164
|
+
|
|
165
|
+
The header prepended to the CHANGELOG comes from a file named `changelog-header.template`.
|
|
166
|
+
|
|
167
|
+
There are no special expando variables supported in the header.
|
|
168
|
+
|
|
169
|
+
##### CHANGELOG Footer Template
|
|
170
|
+
|
|
171
|
+
The footer appended to the CHANGELOG comes from a file named `changelog-footer.template`.
|
|
172
|
+
|
|
173
|
+
There are no special expando variables supported in the footer.
|
|
174
|
+
|
|
175
|
+
##### CHANGELOG Template Caveats
|
|
176
|
+
|
|
177
|
+
No escaping is performed on any of the emitted values. Thus, it is possible for commit messages to interact with parsers/renderers in unintended ways. For example if you make a template to emit an HTML changelog, and a commit message contains content which looks like an 'element', it will most likely result in a changelog that doesn't render as expected.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
.scripts/conventional-semver
|
|
5
|
+
src/conventional_semver/ChangelogOutputGenerator.py
|
|
6
|
+
src/conventional_semver/CommandLineProcessor.py
|
|
7
|
+
src/conventional_semver/Configuration.py
|
|
8
|
+
src/conventional_semver/GitAdapter.py
|
|
9
|
+
src/conventional_semver/GitEntry.py
|
|
10
|
+
src/conventional_semver/GitEntryParser.py
|
|
11
|
+
src/conventional_semver/GitLogStream.py
|
|
12
|
+
src/conventional_semver/OutputGenerator.py
|
|
13
|
+
src/conventional_semver/SemverComponentType.py
|
|
14
|
+
src/conventional_semver/SemverOutputGenerator.py
|
|
15
|
+
src/conventional_semver/__init__.py
|
|
16
|
+
src/conventional_semver/__main__.py
|
|
17
|
+
src/py_conventional_semver.egg-info/PKG-INFO
|
|
18
|
+
src/py_conventional_semver.egg-info/SOURCES.txt
|
|
19
|
+
src/py_conventional_semver.egg-info/dependency_links.txt
|
|
20
|
+
src/py_conventional_semver.egg-info/requires.txt
|
|
21
|
+
src/py_conventional_semver.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
conventional_semver
|