v / cmd / tools / changelog_helper.v
594 lines · 525 sloc · 11.31 KB · dd1ad2b6abfa62a9695e4e04d4827bff137ff94e
Raw
1module main
2
3import os
4
5const delete_skipped = true
6
7const git_log_cmd = 'git log -n 500 --pretty=format:"%s" --simplify-merges'
8
9enum Category {
10 checker
11 breaking
12 improvements
13 parser
14 stdlib
15 web
16 orm
17 db
18 cgen
19 js_backend
20 comptime
21 tools
22 compiler_internals
23 examples
24 vfmt
25 os_support
26 interpreter
27}
28
29const category_titles = '#### Improvements in the language
30
31#### V interpreter
32
33#### Breaking changes
34
35#### Checker improvements/fixes
36
37#### Parser improvements
38
39#### Compiler internals
40
41#### Standard library
42
43#### Web
44
45#### ORM
46
47#### Database drivers
48
49#### C backend
50
51#### JavaScript backend
52
53#### vfmt
54
55#### Tools
56
57#### Operating System support
58
59#### Examples
60'
61
62struct Line {
63 category Category
64 text string
65}
66
67const log_txt = 'log.txt'
68
69struct App {
70 version string // e.g. "0.4.5"
71 total_lines int
72mut:
73 result string // resulting CHANGELOG.md
74 counter int
75}
76
77const is_interactive = false
78
79fn main() {
80 mut version := ''
81
82 if os.args.len == 2 && os.args[1].starts_with('0.') {
83 version = os.args[1]
84 } else {
85 println('Usage: v run tools/changelog_helper.v 0.4.5')
86 return
87 }
88 if !os.exists(log_txt) {
89 os.execute(git_log_cmd + ' > ' + log_txt)
90 println('log.txt generated')
91 // println('log.txt generated, remove unnecessary commits from it and run the tool again')
92 // return
93 }
94 mut lines := os.read_lines(log_txt)!
95 // Trim everything before current version, commit "(tag: 0.4.4) V 0.4.4"
96 mut prev_version := get_prev_version(version)
97 println('prev version=${prev_version}')
98 for i, line in lines {
99 if line == ('V ${prev_version}') {
100 lines = lines[..i].clone()
101 break
102 }
103 }
104 os.write_file(log_txt, lines.join('\n'))!
105 mut app := &App{
106 total_lines: lines.len
107 }
108 // Write categories at the top first
109 app.result = os.read_file('CHANGELOG.md')!.replace_once('V ${version} TODO', 'V ${version}\n' +
110 category_titles)
111 os.write_file('CHANGELOG.md', app.result)!
112 changelog_txt := os.read_file('CHANGELOG.md')!.to_lower()
113 // mut counter := 0 // to display how many commits are left
114 for line in lines {
115 s := line.trim_space()
116 if s == '' {
117 app.counter++
118 }
119 }
120 // println('${counter} / ${lines.len}')
121 for line in lines {
122 s := line.to_lower()
123 if line != '' && (changelog_txt.contains(s) || changelog_txt.contains(s.after(':'))) {
124 println('Duplicate: "${line}"')
125 // skip duplicate
126 delete_processed_line_from_log(line)!
127 continue
128 }
129
130 app.process_line(line.trim_space())!
131 }
132 println('writing changelog.md')
133 if !is_interactive {
134 os.write_file('CHANGELOG.md', app.result)!
135 }
136 println('done.')
137}
138
139fn (mut app App) process_line(text string) ! {
140 if text == '' {
141 return
142 }
143 semicolon_pos := text.index(': ') or {
144 println('no `:` in commit, skipping: "${text}"')
145 return
146 }
147 prefix := text[..semicolon_pos]
148 // Get category based on keywords in the commit message/prefix
149 mut category := Category.examples
150 if text.contains('checker:') {
151 category = .checker
152 } else if is_interpreter(text) {
153 category = .interpreter
154 } else if is_examples(text) {
155 category = .examples
156 // println("Skipping line (example) ${text}")
157 // return
158 } else if is_skip(text) {
159 // Always skip cleanups, typos etc
160 println('Skipping line (cleanup/typo)\n${text}\n')
161 if delete_skipped {
162 delete_processed_line_from_log(text)!
163 }
164 return
165 } else if is_os_support(text) {
166 category = .os_support
167 } else if is_cgen(text) {
168 category = .cgen
169 } else if is_js_backend(text) {
170 category = .js_backend
171 } else if is_comptime(text) {
172 category = .comptime
173 } else if is_db(text) {
174 category = .db
175 } else if is_stdlib(text) {
176 category = .stdlib
177 } else if is_orm(text) {
178 category = .orm
179 } else if is_web(text) {
180 category = .web
181 } else if is_tools(text) {
182 category = .tools
183 } else if is_parser(text) {
184 category = .parser
185 } else if is_internal(text) {
186 category = .compiler_internals
187 } else if is_improvements(text) {
188 category = .improvements
189 } else if is_vfmt(text) {
190 category = .vfmt
191 } else if text.contains('docs:') || text.contains('doc:') {
192 // Always skip docs
193 delete_processed_line_from_log(text)!
194 return
195 } else {
196 println('Skipping line (unknown category)\n${text}\n')
197 // if delete_skipped {
198 // delete_processed_line_from_log(text)!
199 //}
200 return
201 }
202 println('process_line: cat=${category} "${text}"')
203
204 // Trim everything to the left of `:` for some commits (e.g. `checker: `)
205 mut s := text
206 // println("PREFIX='${prefix}'")
207 // if true {
208 // exit(0)
209 //}
210 if (semicolon_pos < 15
211 && prefix in ['checker', 'cgen', 'orm', 'parser', 'v.parser', 'native', 'ast', 'jsgen', 'v.gen.js', 'fmt', 'vfmt', 'tools', 'examples', 'eval'])
212 || (semicolon_pos < 30 && prefix.contains(', ')) {
213 s = '- ' + text[semicolon_pos + 2..].capitalize()
214 }
215
216 if is_interactive {
217 // Get input from the user
218 print('\033[H\033[J')
219 println('${app.counter} / ${app.total_lines}')
220 // println('\n')
221 println(text)
222 input := os.input('${category}? ')
223 println("INPUT='${input}'")
224 match input {
225 '' {
226 println('GOT ENTER')
227 line := Line{category, s}
228 save_line_interactive(line)!
229 }
230 'n', '0', 'no' {
231 // Ignore commit
232 println('ignored.')
233 }
234 's', 'skip' {
235 // Skip
236 println('skipped.')
237 return
238 }
239 'c', 'change' {
240 // Change category
241 for {
242 print_category_hint()
243 custom_category := os.input('${category} ?').int()
244 if custom_category == 0 {
245 println('wrong category')
246 } else {
247 unsafe {
248 line := Line{Category(custom_category - 1), s}
249 save_line_interactive(line)!
250 }
251 break
252 }
253 }
254 }
255 else {}
256 }
257
258 app.counter++
259 } else {
260 line := Line{category, s}
261 app.save_line(line)!
262 }
263 // Don't forget to remove the line we just processed from log.txt
264 delete_processed_line_from_log(text)!
265}
266
267fn (mut app App) save_line(line Line) ! {
268 // println('save line ${line}')
269 app.result = line.write_at_category(app.result) or { return error('') }
270}
271
272fn save_line_interactive(line Line) ! {
273 println('save line interactive ${line}')
274 mut txt := os.read_file('CHANGELOG.md')!
275 txt = line.write_at_category(txt) or { return error('') }
276 os.write_file('CHANGELOG.md', txt)!
277}
278
279const category_map = {
280 Category.checker: '#### Checker improvements'
281 .breaking: '#### Breaking changes'
282 .improvements: '#### Improvements in the'
283 .interpreter: '#### V interpreter'
284 .parser: '#### Parser improvements'
285 .stdlib: '#### Standard library'
286 .web: '#### Web'
287 .orm: '#### ORM'
288 .db: '#### Database drivers'
289 .cgen: '#### C backend'
290 .js_backend: '#### JavaScript backend'
291 .comptime: '#### Comptime'
292 .tools: '#### Tools'
293 .compiler_internals: '#### Compiler internals'
294 .examples: '#### Examples'
295 .vfmt: '#### vfmt'
296 .os_support: '#### Operating System'
297}
298
299fn (l Line) write_at_category(txt string) ?string {
300 title := category_map[l.category]
301 title_pos := txt.index(title)?
302 // Find the position of the ### category title
303 pos := txt.index_after('\n', title_pos + 1) or { return none }
304 first_half := txt[..pos]
305 second_half := txt[pos..]
306 if txt.contains(l.text) {
307 // Avoid duplicates (just in case)
308 println("Got a duplicate: '${txt}'")
309 return txt
310 }
311 // Now insert the line in the middle, under the ### category title
312 mut line_text := l.text
313
314 // Trim "prefix:" for some categories
315 // mut capitalized := false
316 mut has_prefix := true
317 if l.category in [.cgen, .checker, .improvements, .orm, .interpreter] {
318 has_prefix = false
319 if semicolon_pos := line_text.index(': ') {
320 prefix := line_text[..semicolon_pos]
321 println("PREFIX='${prefix}'")
322 if semicolon_pos < 15 {
323 line_text = line_text[semicolon_pos + 2..].capitalize()
324 // capitalized = true
325 }
326 }
327 }
328 if !has_prefix {
329 line_text = line_text.capitalize()
330 }
331 if !line_text.starts_with('- ') {
332 line_text = '- ' + line_text
333 }
334 return first_half + '\n' + line_text + second_half
335}
336
337fn delete_processed_line_from_log(line string) ! {
338 text := os.read_file(log_txt)!
339 new_text := text.replace_once(line, '')
340 os.write_file(log_txt, new_text)!
341}
342
343const db_strings = [
344 'db:',
345 'db.sqlite',
346 'db.mysql',
347 'db.pg',
348 'db.redis',
349 'pg:',
350 'mysql:',
351]
352
353const parser_strings = [
354 'parser:',
355 'ast:',
356]
357
358const stdlib_strings = [
359 'gg:',
360 'json:',
361 'json2:',
362 'time:',
363 'sync:',
364 'datatypes:',
365 'math:',
366 'math.',
367 'math.big',
368 'crypto',
369 'sokol',
370 'os:',
371 'rand:',
372 'rand.',
373 'math:',
374 'toml:',
375 'vlib:',
376 'arrays:',
377 'os.',
378 'term:',
379 'sync.',
380 'builtin:',
381 'builtin,',
382 'builtin.',
383 'strconv',
384 'readline',
385 'cli:',
386 'eventbus:',
387 'encoding.',
388 'bitfield:',
389 'io:',
390 'io.',
391 'log:',
392 'flag:',
393 'regex:',
394 'regex.',
395 'tmpl:',
396 'hash:',
397 'stbi:',
398 'atomic:',
399 'context:',
400 'thirdparty',
401]
402
403fn is_stdlib(text string) bool {
404 return is_xxx(text, stdlib_strings)
405}
406
407fn is_db(text string) bool {
408 return is_xxx(text, db_strings)
409}
410
411const orm_strings = [
412 'orm:',
413]
414
415fn is_orm(text string) bool {
416 return is_xxx(text, orm_strings)
417}
418
419const cgen_strings = [
420 'cgen:',
421 'cgen,',
422 'v.gen.c:',
423]
424
425fn is_cgen(text string) bool {
426 return is_xxx(text, cgen_strings)
427}
428
429const js_backend_strings = [
430 'js:',
431 'v.gen.js:',
432 'jsgen:',
433]
434
435fn is_js_backend(text string) bool {
436 return is_xxx(text, js_backend_strings)
437}
438
439const internal_strings = [
440 'scanner:',
441 'transformer:',
442 'markused:',
443 'builder:',
444 'pref:',
445 'v.util',
446 'v.generic',
447 'v.comptime',
448 'table:',
449]
450
451fn is_internal(text string) bool {
452 return is_xxx(text, internal_strings)
453}
454
455const improvements_strings = [
456 'all:',
457 'v:',
458 'coroutines:',
459 'autofree',
460]
461
462fn is_improvements(text string) bool {
463 return is_xxx(text, improvements_strings)
464}
465
466const examples_strings = [
467 'example',
468]
469const skip_strings = [
470 'tests',
471 'readme:',
472 '.md:',
473 'typos',
474 ' typo',
475 'cleanup',
476 'clean up',
477 'build(deps)',
478 'FUNDING',
479]
480
481fn is_examples(text string) bool {
482 return is_xxx(text, examples_strings)
483}
484
485fn is_skip(text string) bool {
486 return is_xxx(text, skip_strings)
487}
488
489const tools_strings = [
490 'tools:',
491 'vpm:',
492 'ci:',
493 'github:',
494 'gitignore',
495 'benchmark',
496 'v.help:',
497 'vtest',
498 'repl',
499 'REPL',
500 'vet',
501 'tools.',
502 'GNUmakefile',
503 'Dockerfile',
504 'vcomplete',
505 'vwatch',
506 'changelog',
507]
508
509fn is_tools(text string) bool {
510 return is_xxx(text, tools_strings)
511}
512
513fn is_parser(text string) bool {
514 return is_xxx(text, parser_strings)
515}
516
517const web_strings = [
518 'veb',
519 'websocket:',
520 'pico',
521 'x.sessions',
522 'picoev:',
523 'mbedtls',
524 'net:',
525 'net.',
526 'wasm:',
527 'http:',
528]
529
530fn is_web(text string) bool {
531 return is_xxx(text, web_strings)
532}
533
534const vfmt_strings = [
535 'vfmt:',
536 'fmt:',
537]
538
539fn is_vfmt(text string) bool {
540 return is_xxx(text, vfmt_strings)
541}
542
543const os_support_strings = [
544 'FreeBSD',
545 'freebsd',
546 'OpenBSD',
547 'openbsd',
548 'macOS',
549 'macos',
550 'Windows',
551 'windows',
552 'Linux',
553 'linux',
554 'msvc:',
555]
556
557fn is_os_support(text string) bool {
558 return is_xxx(text, os_support_strings)
559}
560
561fn is_comptime(text string) bool {
562 return text.contains('comptime:')
563}
564
565fn is_interpreter(text string) bool {
566 return text.starts_with('eval:')
567}
568
569fn is_xxx(text string, words []string) bool {
570 for s in words {
571 if text.contains(s) {
572 return true
573 }
574 }
575 return false
576}
577
578fn print_category_hint() {
579 $for val in Category.values {
580 println('${int(val.value) + 1} - ${val.name}; ')
581 }
582}
583
584// For 0.4.12 returns 0.4.11 etc
585fn get_prev_version(version string) string {
586 parts := version.split('.')
587 if parts.len != 3 {
588 return ''
589 }
590 major := parts[0]
591 minor := parts[1]
592 patch := parts[2].int()
593 return '${major}.${minor}.${patch - 1}'
594}
595