1
1
"""
2
2
# DESIGN
3
3
4
- ## Parse CHANGELOG.md
4
+ ## Metadata CHANGELOG.md
5
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
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)
10
9
11
10
## Parse git log
12
11
13
12
1. get commits between versions
14
13
2. filter commits with the current cz rules
15
14
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
17
19
18
- Options :
20
+ Extra :
19
21
- Generate full or partial changelog
22
+ - Include in tree from file all the extra comments added manually
20
23
"""
21
24
import re
22
- from typing import Dict , Generator , Iterable , List
25
+ from collections import defaultdict
26
+ from typing import Dict , Iterable , List , Optional
23
27
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
30
30
31
+ from commitizen import defaults
32
+ from commitizen .git import GitCommit , GitProtocol , GitTag
31
33
32
34
CATEGORIES = [
33
35
("fix" , "fix" ),
42
44
]
43
45
44
46
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
-
100
47
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.
101
50
_change_type_lower = change_type .lower ()
102
51
for match_value , output in CATEGORIES :
103
52
if re .search (match_value , _change_type_lower ):
@@ -106,28 +55,80 @@ def transform_change_type(change_type: str) -> str:
106
55
raise ValueError (f"Could not match a change_type with { change_type } " )
107
56
108
57
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
0 commit comments