collect_typos.lua (4607B)
1 #!/usr/bin/env -S nvim -l 2 3 local function die(msg) 4 print(msg) 5 vim.cmd('cquit 1') 6 end 7 8 --- Executes and returns the output of `cmd`, or nil on failure. 9 --- if die_on_fail is true, process dies with die_msg on failure 10 --- @param cmd string[] 11 --- @param die_on_fail boolean 12 --- @param die_msg string 13 --- @param stdin string? 14 --- 15 --- @return string? 16 local function _run(cmd, die_on_fail, die_msg, stdin) 17 local rv = vim.system(cmd, { stdin = stdin }):wait() 18 if rv.code ~= 0 then 19 if rv.stdout:len() > 0 then 20 print(rv.stdout) 21 end 22 if rv.stderr:len() > 0 then 23 print(rv.stderr) 24 end 25 if die_on_fail then 26 die(die_msg) 27 end 28 return nil 29 end 30 return rv.stdout 31 end 32 33 --- Run a command, return nil on failure 34 --- @param cmd string[] 35 --- @param stdin string? 36 --- 37 --- @return string? 38 local function run(cmd, stdin) 39 return _run(cmd, false, '', stdin) 40 end 41 42 --- Run a command, die on failure with err_msg 43 --- @param cmd string[] 44 --- @param err_msg string 45 --- @param stdin string? 46 --- 47 --- @return string 48 local function run_die(cmd, err_msg, stdin) 49 return assert(_run(cmd, true, err_msg, stdin)) 50 end 51 52 --- MIME-decode if python3 is available, else returns the input unchanged. 53 local function mime_decode(encoded) 54 local has_python = vim.system({ 'python3', '--version' }, { text = true }):wait() 55 if has_python.code ~= 0 then 56 return encoded 57 end 58 59 local pycode = string.format( 60 vim.text.indent( 61 0, 62 [[ 63 import sys 64 from email.header import decode_header 65 inp = %q 66 parts = [] 67 for txt, cs in decode_header(inp): 68 if isinstance(txt, bytes): 69 try: 70 parts.append(txt.decode(cs or "utf-8", errors="replace")) 71 except Exception: 72 parts.append(txt.decode("utf-8", errors="replace")) 73 else: 74 parts.append(txt) 75 sys.stdout.write("".join(parts)) 76 ]] 77 ), 78 encoded 79 ) 80 81 local result = vim.system({ 'python3', '-c', pycode }, { text = true }):wait() 82 83 if result.code ~= 0 or not result.stdout then 84 return encoded 85 end 86 87 -- Trim trailing newline Python prints only if program prints it 88 return vim.trim(result.stdout) 89 end 90 91 local function get_commit_msg(close_pr_lines, co_author_lines) 92 return ('docs: misc\n\n%s\n\n%s\n'):format( 93 table.concat(close_pr_lines, '\n'), 94 table.concat(co_author_lines, '\n') 95 ) 96 end 97 98 local function get_fail_msg(msg, pr_number, close_pr_lines, co_author_lines) 99 return ('%s %s\n\nPending commit message:\n%s'):format( 100 msg, 101 pr_number or '', 102 get_commit_msg(close_pr_lines, co_author_lines) 103 ) 104 end 105 106 local function main() 107 local pr_list = vim.json.decode( 108 run_die( 109 { 'gh', 'pr', 'list', '--label', 'typo', '--json', 'number' }, 110 'Failed to get list of typo PRs' 111 ) 112 ) 113 --- @type integer[] 114 local pr_numbers = vim 115 .iter(pr_list) 116 :map(function(pr) 117 return pr.number 118 end) 119 :totable() 120 table.sort(pr_numbers) 121 122 local close_pr_lines = {} 123 local co_author_lines = {} 124 for _, pr_number in ipairs(pr_numbers) do 125 print(('PR #%s'):format(pr_number)) 126 local patch_file = run_die( 127 { 'gh', 'pr', 'diff', tostring(pr_number), '--patch' }, 128 get_fail_msg('Failed to get patch for PR', pr_number, close_pr_lines, co_author_lines) 129 ) 130 -- Using --3way allows skipping changes already included in a previous commit. 131 -- If there are conflicts, it will fail and need manual conflict resolution. 132 if run({ 'git', 'apply', '--index', '--3way', '-' }, patch_file) then 133 table.insert(close_pr_lines, ('Close #%d'):format(pr_number)) 134 for author in patch_file:gmatch('\nFrom: (.- <.->)\n') do 135 local co_author_line = ('Co-authored-by: %s'):format(mime_decode(author)) 136 if not vim.list_contains(co_author_lines, co_author_line) then 137 table.insert(co_author_lines, co_author_line) 138 end 139 end 140 for author in patch_file:gmatch('\nCo%-authored%-by: (.- <.->)\n') do 141 local co_author_line = ('Co-authored-by: %s'):format(mime_decode(author)) 142 if not vim.list_contains(co_author_lines, co_author_line) then 143 table.insert(co_author_lines, co_author_line) 144 end 145 end 146 else 147 print( 148 get_fail_msg('Failed to apply patch for PR', pr_number, close_pr_lines, co_author_lines) 149 ) 150 end 151 end 152 153 local msg = get_commit_msg(close_pr_lines, co_author_lines) 154 print( 155 run_die( 156 { 'git', 'commit', '--file', '-' }, 157 get_fail_msg('Failed to create commit', nil, close_pr_lines, co_author_lines), 158 msg 159 ) 160 ) 161 end 162 163 main()