comparison garcon/changelog.py @ 642:79aefe82c6b6

cm: Move all scripts into a `garcon` package with `invoke` support.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 14 Feb 2016 22:11:24 -0800
parents
children e6d9eed35c8e
comparison
equal deleted inserted replaced
641:35221f5fe0dd 642:79aefe82c6b6
1 import os
2 import os.path
3 import re
4 import time
5 import argparse
6 import subprocess
7
8
9 hg_log_template = ("{if(tags, '>>{tags};{date|shortdate}\n')}"
10 "{desc|firstline}\n\n")
11
12 re_add_tag_changeset = re.compile('^Added tag [^\s]+ for changeset [\w\d]+$')
13 re_merge_pr_changeset = re.compile('^Merge pull request')
14 re_tag = re.compile('^\d+\.\d+\.\d+([ab]\d+)?(rc\d+)?$')
15 re_change = re.compile('^(\w+):')
16 re_clean_code_span = re.compile('([^\s])``([^\s]+)')
17
18 category_commands = [
19 'chef', 'bake', 'find', 'help', 'import', 'init', 'paths', 'plugin',
20 'plugins', 'prepare', 'purge', 'root', 'routes', 'serve',
21 'showconfig', 'showrecord', 'sources', 'theme', 'themes', 'admin',
22 'publish']
23 category_core = [
24 'internal', 'bug', 'templating', 'formatting', 'performance',
25 'data', 'config', 'rendering', 'render', 'debug', 'reporting',
26 'linker', 'pagination', 'routing', 'caching', 'cli']
27 category_project = ['build', 'cm', 'docs', 'tests', 'setup']
28 categories = [
29 ('commands', category_commands),
30 ('core', category_core),
31 ('project', category_project),
32 ('miscellaneous', None)]
33 category_names = list(map(lambda i: i[0], categories))
34
35
36 def generate(out_file, last=None):
37 print("Generating %s" % out_file)
38
39 if not os.path.exists('.hg'):
40 raise Exception("You must run this script from the root of a "
41 "Mercurial clone of the PieCrust repository.")
42 hglog = subprocess.check_output([
43 'hg', 'log',
44 '--rev', 'reverse(::master)',
45 '--template', hg_log_template])
46 hglog = hglog.decode('utf8')
47
48 templates = _get_templates()
49
50 with open(out_file, 'w') as fp:
51 fp.write(templates['header'])
52
53 skip = False
54 in_desc = False
55 current_version = 0
56 current_version_info = None
57 current_changes = None
58
59 if last:
60 current_version = 1
61 cur_date = time.strftime('%Y-%m-%d')
62 current_version_info = last, cur_date
63 current_changes = {}
64
65 for line in hglog.splitlines():
66 if line == '':
67 skip = False
68 in_desc = False
69 continue
70
71 if not in_desc and line.startswith('>>'):
72 tags, tag_date = line[2:].split(';')
73 if re_tag.match(tags):
74 if current_version > 0:
75 _write_version_changes(
76 templates,
77 current_version, current_version_info,
78 current_changes, fp)
79
80 current_version += 1
81 current_version_info = tags, tag_date
82 current_changes = {}
83 in_desc = True
84 else:
85 skip = True
86 continue
87
88 if skip or current_version == 0:
89 continue
90
91 if re_add_tag_changeset.match(line):
92 continue
93 if re_merge_pr_changeset.match(line):
94 continue
95
96 m = re_change.match(line)
97 if m:
98 ch_type = m.group(1)
99 for cat_name, ch_types in categories:
100 if ch_types is None or ch_type in ch_types:
101 msgs = current_changes.setdefault(cat_name, [])
102 msgs.append(line)
103 break
104 else:
105 assert False, ("Change '%s' should have gone in the "
106 "misc. bucket." % line)
107 else:
108 msgs = current_changes.setdefault('miscellaneous', [])
109 msgs.append(line)
110
111 if current_version > 0:
112 _write_version_changes(
113 templates,
114 current_version, current_version_info,
115 current_changes, fp)
116
117
118 def _write_version_changes(templates, version, version_info, changes, fp):
119 tokens = {
120 'num': str(version),
121 'version': version_info[0],
122 'date': version_info[1]}
123 tpl = _multi_replace(templates['version_title'], tokens)
124 fp.write(tpl)
125
126 for i, cat_name in enumerate(category_names):
127 msgs = changes.get(cat_name)
128 if not msgs:
129 continue
130
131 tokens = {
132 'sub_num': str(i),
133 'category': cat_name.title()}
134 tpl = _multi_replace(templates['category_title'], tokens)
135 fp.write(tpl)
136
137 for msg in msgs:
138 msg = msg.replace('`', '``').rstrip('\n')
139 msg = re_clean_code_span.sub(r'\1`` \2', msg)
140 fp.write('* ' + msg + '\n')
141
142
143 def _multi_replace(s, tokens):
144 for token in tokens:
145 s = s.replace('%%%s%%' % token, tokens[token])
146 return s
147
148
149 def _get_templates():
150 tpl_dir = os.path.join(os.path.dirname(__file__), 'changelog')
151 tpls = {}
152 for name in os.listdir(tpl_dir):
153 tpl = _get_template(os.path.join(tpl_dir, name))
154 name_no_ext, _ = os.path.splitext(name)
155 tpls[name_no_ext] = tpl
156 return tpls
157
158
159 def _get_template(filename):
160 with open(filename, 'r', encoding='utf8') as fp:
161 return fp.read()
162
163
164 if __name__ == '__main__':
165 parser = argparse.ArgumentParser(description='Generate CHANGELOG file.')
166 parser.add_argument(
167 'out_file',
168 nargs='?',
169 default='CHANGELOG.rst',
170 help='The output file.')
171 parser.add_argument(
172 '--last',
173 help="The version for the last few untagged changes.")
174 args = parser.parse_args()
175
176 generate(args.out_file, last=args.last)
177 else:
178 from invoke import task
179
180 @task
181 def genchangelog(out_file='CHANGELOG.rst', last=None):
182 generate(out_file, last)
183