Skip to content

Commit e0a1b49

Browse files
committed
refactor(changelog): use functions from changelog.py
1 parent 65d8268 commit e0a1b49

16 files changed

+1816
-420
lines changed

CHANGELOG.md

Lines changed: 434 additions & 69 deletions
Large diffs are not rendered by default.

commitizen/changelog.py

Lines changed: 95 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
11
"""
22
# DESIGN
33
4-
## Parse CHANGELOG.md
4+
## Metadata CHANGELOG.md
55
6-
1. Get LATEST VERSION from CONFIG
7-
1. Parse the file version to version
8-
2. Build a dict (tree) of that particular version
9-
3. Transform tree into markdown again
6+
1. Identify irrelevant information (possible: changelog title, first paragraph)
7+
2. Identify Unreleased area
8+
3. Identify latest version (to be able to write on top of it)
109
1110
## Parse git log
1211
1312
1. get commits between versions
1413
2. filter commits with the current cz rules
1514
3. parse commit information
16-
4. generate tree
15+
4. yield tree nodes
16+
5. format tree nodes
17+
6. produce full tree
18+
7. generate changelog
1719
18-
Options:
20+
Extra:
1921
- Generate full or partial changelog
22+
- Include in tree from file all the extra comments added manually
2023
"""
2124
import re
22-
from typing import Dict, Generator, Iterable, List
25+
from collections import defaultdict
26+
from typing import Dict, Iterable, List, Optional
2327

24-
MD_VERSION_RE = r"^##\s(?P<version>[a-zA-Z0-9.+]+)\s?\(?(?P<date>[0-9-]+)?\)?"
25-
MD_CHANGE_TYPE_RE = r"^###\s(?P<change_type>[a-zA-Z0-9.+\s]+)"
26-
MD_MESSAGE_RE = r"^-\s(\*{2}(?P<scope>[a-zA-Z0-9]+)\*{2}:\s)?(?P<message>.+)"
27-
md_version_c = re.compile(MD_VERSION_RE)
28-
md_change_type_c = re.compile(MD_CHANGE_TYPE_RE)
29-
md_message_c = re.compile(MD_MESSAGE_RE)
28+
import pkg_resources
29+
from jinja2 import Template
3030

31+
from commitizen import defaults
32+
from commitizen.git import GitCommit, GitProtocol, GitTag
3133

3234
CATEGORIES = [
3335
("fix", "fix"),
@@ -42,62 +44,9 @@
4244
]
4345

4446

45-
def find_version_blocks(filepath: str) -> Generator:
46-
"""
47-
version block: contains all the information about a version.
48-
49-
E.g:
50-
```
51-
## 1.2.1 (2019-07-20)
52-
53-
## Bug fixes
54-
55-
- username validation not working
56-
57-
## Features
58-
59-
- new login system
60-
61-
```
62-
"""
63-
with open(filepath, "r") as f:
64-
block: list = []
65-
for line in f:
66-
line = line.strip("\n")
67-
if not line:
68-
continue
69-
70-
if line.startswith("## "):
71-
if len(block) > 0:
72-
yield block
73-
block = [line]
74-
else:
75-
block.append(line)
76-
yield block
77-
78-
79-
def parse_md_version(md_version: str) -> Dict:
80-
m = md_version_c.match(md_version)
81-
if not m:
82-
return {}
83-
return m.groupdict()
84-
85-
86-
def parse_md_change_type(md_change_type: str) -> Dict:
87-
m = md_change_type_c.match(md_change_type)
88-
if not m:
89-
return {}
90-
return m.groupdict()
91-
92-
93-
def parse_md_message(md_message: str) -> Dict:
94-
m = md_message_c.match(md_message)
95-
if not m:
96-
return {}
97-
return m.groupdict()
98-
99-
10047
def transform_change_type(change_type: str) -> str:
48+
# TODO: Use again to parse, for this we have to wait until the maps get
49+
# defined again.
10150
_change_type_lower = change_type.lower()
10251
for match_value, output in CATEGORIES:
10352
if re.search(match_value, _change_type_lower):
@@ -106,28 +55,80 @@ def transform_change_type(change_type: str) -> str:
10655
raise ValueError(f"Could not match a change_type with {change_type}")
10756

10857

109-
def generate_block_tree(block: List[str]) -> Dict:
110-
tree: Dict = {"commits": []}
111-
change_type = None
112-
for line in block:
113-
if line.startswith("## "):
114-
change_type = None
115-
tree = {**tree, **parse_md_version(line)}
116-
elif line.startswith("### "):
117-
result = parse_md_change_type(line)
118-
if not result:
119-
continue
120-
change_type = transform_change_type(result.get("change_type", ""))
121-
122-
elif line.startswith("- "):
123-
commit = parse_md_message(line)
124-
commit["change_type"] = change_type
125-
tree["commits"].append(commit)
126-
else:
127-
print("it's something else: ", line)
128-
return tree
129-
130-
131-
def generate_full_tree(blocks: Iterable) -> Iterable[Dict]:
132-
for block in blocks:
133-
yield generate_block_tree(block)
58+
def get_commit_tag(commit: GitProtocol, tags: List[GitProtocol]) -> Optional[GitTag]:
59+
""""""
60+
try:
61+
tag_index = tags.index(commit)
62+
except ValueError:
63+
return None
64+
else:
65+
tag = tags[tag_index]
66+
return tag
67+
68+
69+
def generate_tree_from_commits(
70+
commits: List[GitCommit],
71+
tags: List[GitTag],
72+
commit_parser: str,
73+
changelog_pattern: str = defaults.bump_pattern,
74+
) -> Iterable[Dict]:
75+
pat = re.compile(changelog_pattern)
76+
map_pat = re.compile(commit_parser)
77+
# Check if the latest commit is not tagged
78+
latest_commit = commits[0]
79+
current_tag: Optional[GitTag] = get_commit_tag(latest_commit, tags)
80+
81+
current_tag_name: str = "Unreleased"
82+
current_tag_date: str = ""
83+
if current_tag is not None and current_tag.name:
84+
current_tag_name = current_tag.name
85+
current_tag_date = current_tag.date
86+
87+
changes: Dict = defaultdict(list)
88+
used_tags: List = [current_tag]
89+
for commit in commits:
90+
commit_tag = get_commit_tag(commit, tags)
91+
92+
if commit_tag is not None and commit_tag not in used_tags:
93+
used_tags.append(commit_tag)
94+
yield {
95+
"version": current_tag_name,
96+
"date": current_tag_date,
97+
"changes": changes,
98+
}
99+
# TODO: Check if tag matches the version pattern, otherwie skip it.
100+
# This in order to prevent tags that are not versions.
101+
current_tag_name = commit_tag.name
102+
current_tag_date = commit_tag.date
103+
changes = defaultdict(list)
104+
105+
matches = pat.match(commit.message)
106+
if not matches:
107+
continue
108+
109+
message = map_pat.match(commit.message)
110+
message_body = map_pat.match(commit.body)
111+
if message:
112+
parsed_message: Dict = message.groupdict()
113+
# change_type becomes optional by providing None
114+
change_type = parsed_message.pop("change_type", None)
115+
changes[change_type].append(parsed_message)
116+
if message_body:
117+
parsed_message_body: Dict = message_body.groupdict()
118+
change_type = parsed_message_body.pop("change_type", None)
119+
changes[change_type].append(parsed_message_body)
120+
121+
yield {
122+
"version": current_tag_name,
123+
"date": current_tag_date,
124+
"changes": changes,
125+
}
126+
127+
128+
def render_changelog(tree: Iterable) -> str:
129+
template_file = pkg_resources.resource_string(
130+
__name__, "templates/keep_a_changelog_template.j2"
131+
).decode("utf-8")
132+
jinja_template = Template(template_file, trim_blocks=True)
133+
changelog: str = jinja_template.render(tree=tree)
134+
return changelog

commitizen/changelog_parser.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""
2+
# DESIGN
3+
4+
## Parse CHANGELOG.md
5+
6+
1. Get LATEST VERSION from CONFIG
7+
1. Parse the file version to version
8+
2. Build a dict (tree) of that particular version
9+
3. Transform tree into markdown again
10+
"""
11+
import re
12+
from collections import defaultdict
13+
from typing import Dict, Generator, Iterable, List
14+
15+
MD_VERSION_RE = r"^##\s(?P<version>[a-zA-Z0-9.+]+)\s?\(?(?P<date>[0-9-]+)?\)?"
16+
MD_CHANGE_TYPE_RE = r"^###\s(?P<change_type>[a-zA-Z0-9.+\s]+)"
17+
MD_MESSAGE_RE = (
18+
r"^-\s(\*{2}(?P<scope>[a-zA-Z0-9]+)\*{2}:\s)?(?P<message>.+)(?P<breaking>!)?"
19+
)
20+
md_version_c = re.compile(MD_VERSION_RE)
21+
md_change_type_c = re.compile(MD_CHANGE_TYPE_RE)
22+
md_message_c = re.compile(MD_MESSAGE_RE)
23+
24+
25+
CATEGORIES = [
26+
("fix", "fix"),
27+
("breaking", "BREAKING CHANGES"),
28+
("feat", "feat"),
29+
("refactor", "refactor"),
30+
("perf", "perf"),
31+
("test", "test"),
32+
("build", "build"),
33+
("ci", "ci"),
34+
("chore", "chore"),
35+
]
36+
37+
38+
def find_version_blocks(filepath: str) -> Generator:
39+
"""
40+
version block: contains all the information about a version.
41+
42+
E.g:
43+
```
44+
## 1.2.1 (2019-07-20)
45+
46+
### Fix
47+
48+
- username validation not working
49+
50+
### Feat
51+
52+
- new login system
53+
54+
```
55+
"""
56+
with open(filepath, "r") as f:
57+
block: list = []
58+
for line in f:
59+
line = line.strip("\n")
60+
if not line:
61+
continue
62+
63+
if line.startswith("## "):
64+
if len(block) > 0:
65+
yield block
66+
block = [line]
67+
else:
68+
block.append(line)
69+
yield block
70+
71+
72+
def parse_md_version(md_version: str) -> Dict:
73+
m = md_version_c.match(md_version)
74+
if not m:
75+
return {}
76+
return m.groupdict()
77+
78+
79+
def parse_md_change_type(md_change_type: str) -> Dict:
80+
m = md_change_type_c.match(md_change_type)
81+
if not m:
82+
return {}
83+
return m.groupdict()
84+
85+
86+
def parse_md_message(md_message: str) -> Dict:
87+
m = md_message_c.match(md_message)
88+
if not m:
89+
return {}
90+
return m.groupdict()
91+
92+
93+
def transform_change_type(change_type: str) -> str:
94+
# TODO: Use again to parse, for this we have to wait until the maps get
95+
# defined again.
96+
_change_type_lower = change_type.lower()
97+
for match_value, output in CATEGORIES:
98+
if re.search(match_value, _change_type_lower):
99+
return output
100+
else:
101+
raise ValueError(f"Could not match a change_type with {change_type}")
102+
103+
104+
def generate_block_tree(block: List[str]) -> Dict:
105+
# tree: Dict = {"commits": []}
106+
changes: Dict = defaultdict(list)
107+
tree: Dict = {"changes": changes}
108+
109+
change_type = None
110+
for line in block:
111+
if line.startswith("## "):
112+
# version identified
113+
change_type = None
114+
tree = {**tree, **parse_md_version(line)}
115+
elif line.startswith("### "):
116+
# change_type identified
117+
result = parse_md_change_type(line)
118+
if not result:
119+
continue
120+
change_type = result.get("change_type", "").lower()
121+
122+
elif line.startswith("- "):
123+
# message identified
124+
commit = parse_md_message(line)
125+
changes[change_type].append(commit)
126+
else:
127+
print("it's something else: ", line)
128+
return tree
129+
130+
131+
def generate_full_tree(blocks: Iterable) -> Iterable[Dict]:
132+
for block in blocks:
133+
yield generate_block_tree(block)

commitizen/cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@
131131
"default": False,
132132
"help": "show changelog to stdout",
133133
},
134+
{
135+
"name": "--incremental",
136+
"action": "store_true",
137+
"default": False,
138+
"help": (
139+
"generates changelog from last created version, "
140+
"useful if the changelog has been manually modified"
141+
),
142+
},
134143
{
135144
"name": "--file-name",
136145
"help": "file name of changelog (default: 'CHANGELOG.md')",

0 commit comments

Comments
 (0)