neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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()