module main import os const git_log_cmd = 'git log -n 500 --pretty=format:"%s" --simplify-merges' enum Category { checker breaking improvements parser stdlib web orm db native cgen comptime tools compiler_internals examples vfmt } /* #### Improvements in the language #### Breaking changes #### Checker improvements/fixes #### Parser improvements #### Compiler internals #### Standard library #### Web #### ORM #### Database drivers #### Native backend #### C backend #### vfmt #### Tools #### Operating System support #### Examples */ struct Line { category Category text string } const log_txt = 'log.txt' struct App { total_lines int mut: counter int } fn main() { if !os.exists(log_txt) { os.execute(git_log_cmd + ' > ' + log_txt) println('log.txt generated, remove unnecessary commits from it and run the tool again') return } lines := os.read_lines(log_txt)! changelog_txt := os.read_file('CHANGELOG.md')!.to_lower() mut app := &App{ total_lines: lines.len } // mut counter := 0 // to display how many commits are left for line in lines { s := line.trim_space() if s == '' { app.counter++ } } // println('${counter} / ${lines.len}') for line in lines { s := line.to_lower() if line != '' && (changelog_txt.contains(s) || changelog_txt.contains(s.after(':'))) { println('Duplicate: "${line}"') // skip duplicate delete_processed_line_from_log(line)! continue } app.process_line(line)! } println('done.') } fn (mut app App) process_line(text string) ! { if text == '' { return } semicolon_pos := text.index(': ') or { println('no `:` in commit, skipping: "${text}"') return } prefix := text[..semicolon_pos] // Get category based on keywords in the commit message/prefix mut category := Category.improvements if text.contains('checker:') { category = .checker } else if text.contains('cgen:') { category = .cgen } else if is_db(text) { category = .db } else if is_stdlib(text) { category = .stdlib } else if is_orm(text) { category = .orm } else if is_web(text) { category = .web } else if is_tools(text) { category = .tools } else if is_parser(text) { category = .parser } else if is_internal(text) { category = .compiler_internals } else if is_improvements(text) { category = .improvements } else if is_native(text) { category = .native } else if is_vfmt(text) { category = .vfmt } else if is_examples(text) { // TODO maybe always skip these as well? category = .examples } else if text.contains('docs:') || text.contains('doc:') { // Always skip docs delete_processed_line_from_log(text)! return } // else { return } // Trim everything to the left of `:` for some commits (e.g. `checker: `) mut s := text // println("PREFIX='${prefix}'") // if true { // exit(0) //} if semicolon_pos < 15 && prefix in ['checker', 'cgen'] { s = '- ' + text[semicolon_pos + 2..].capitalize() } // Get input from the user print('\033[H\033[J') println('${app.counter} / ${app.total_lines}') // println('\n') println(text) input := os.input('${category} ?') println("INPUT='${input}'") match input { '' { println('GOT ENTER') line := Line{category, s} save_line(line)! } 'n', '0', 'no' { // Ignore commit println('ignored.') } 's', 'skip' { // Skip println('skipped.') return } 'c', 'change' { // Change category for { print_category_hint() custom_category := os.input('${category} ?').int() if custom_category == 0 { println('wrong category') } else { unsafe { line := Line{Category(custom_category - 1), s} save_line(line)! } break } } } else {} } app.counter++ // Don't forget to remove the line we just processed from log.txt delete_processed_line_from_log(text)! } fn save_line(line Line) ! { println('save line ${line}') mut txt := os.read_file('CHANGELOG.md')! // match line.category { //.checker { txt = line.write_at_category(txt) or { return error('') } // println(txt.limit(1000)) //} // else {} //} os.write_file('CHANGELOG.md', txt)! } const category_map = { Category.checker: '#### Checker improvements' .breaking: '#### Breaking changes' .improvements: '#### Improvements in the' .parser: '#### Parser improvements' .stdlib: '#### Standard library' .web: '#### Web' .orm: '#### ORM' .db: '#### Database drivers' .native: '#### Native backend' .cgen: '#### C backend' .comptime: '#### Comptime' .tools: '#### Tools' .compiler_internals: '#### Compiler internals' .examples: '#### Examples' .vfmt: '#### vfmt' } fn (l Line) write_at_category(txt string) ?string { title := category_map[l.category] title_pos := txt.index(title)? // Find the position of the ### category title pos := txt.index_after('\n', title_pos + 1) first_half := txt[..pos] second_half := txt[pos..] if txt.contains(l.text) { // Avoid duplicates (just in case) println("Got a duplicate: '${txt}'") return txt } // Now insert the line in the middle, under the ### category title mut line_text := l.text // Trim "prefix:" for some categories // mut capitalized := false mut has_prefix := true if l.category in [.cgen, .checker, .improvements] { has_prefix = false if semicolon_pos := line_text.index(': ') { prefix := line_text[..semicolon_pos] println("PREFIX='${prefix}'") if semicolon_pos < 15 { line_text = line_text[semicolon_pos + 2..].capitalize() // capitalized = true } } } if !has_prefix { line_text = line_text.capitalize() } if !line_text.starts_with('- ') { line_text = '- ' + line_text } return first_half + '\n' + line_text + second_half } fn delete_processed_line_from_log(line string) ! { text := os.read_file(log_txt)! new_text := text.replace_once(line, '') os.write_file(log_txt, new_text)! } const db_strings = [ 'db:', 'db.sqlite', 'db.mysql', 'db.pg', 'pg:', ] const improvements_strings = [ 'all:', 'v:', ] const parser_strings = [ 'parser:', 'ast:', ] const stdlib_strings = [ 'gg:', 'json:', 'json2:', 'time:', 'sync:', 'datatypes:', 'math:', 'math.big', 'crypto', 'sokol:', 'os:', 'rand:', 'math:', 'toml:', 'vlib:', 'arrays:', 'math.', 'os.', 'term:', 'sync.', 'builtin:', 'strconv', 'readline', 'cli:', 'eventbus:', ] fn is_stdlib(text string) bool { return is_xxx(text, stdlib_strings) } fn is_db(text string) bool { return is_xxx(text, db_strings) } const orm_strings = [ 'orm:', ] fn is_orm(text string) bool { return is_xxx(text, orm_strings) } const internal_strings = [ 'scanner:', 'transformer:', 'builder:', 'pref:', 'v.util', ] fn is_internal(text string) bool { return is_xxx(text, internal_strings) } fn is_improvements(text string) bool { return is_xxx(text, improvements_strings) } const examples_strings = [ 'example', 'tests', 'readme:', '.md:', ] fn is_examples(text string) bool { return is_xxx(text, examples_strings) } const tools_strings = [ 'tools:', 'vpm:', 'ci', 'github:', 'gitignore', 'benchmark', 'v.help:', ] fn is_tools(text string) bool { return is_xxx(text, tools_strings) } fn is_parser(text string) bool { return is_xxx(text, parser_strings) } const web_strings = [ 'vweb:', 'websocket:', 'picoev:', 'mbedtls', 'net:', 'net.', 'wasm:', 'http:', ] fn is_web(text string) bool { return is_xxx(text, web_strings) } const native_strings = [ 'native:', ] fn is_native(text string) bool { return is_xxx(text, native_strings) } const vfmt_strings = [ 'vfmt:', 'fmt:', ] fn is_vfmt(text string) bool { return is_xxx(text, vfmt_strings) } fn is_xxx(text string, words []string) bool { for s in words { if text.contains(s) { return true } } return false } fn print_category_hint() { $for val in Category.values { println('${int(val.value) + 1} - ${val.name}; ') } }