summaryrefslogtreecommitdiffstatshomepage
path: root/scripts/collect_typos.lua
blob: d5d3128c93b41ffacab9e95a486839f75eae4f30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/env -S nvim -l

local function die(msg)
  print(msg)
  vim.cmd('cquit 1')
end

--- Executes and returns the output of `cmd`, or nil on failure.
--- if die_on_fail is true, process dies with die_msg on failure
--- @param cmd string[]
--- @param die_on_fail boolean
--- @param die_msg string
--- @param stdin string?
---
--- @return string?
local function _run(cmd, die_on_fail, die_msg, stdin)
  local rv = vim.system(cmd, { stdin = stdin }):wait()
  if rv.code ~= 0 then
    if rv.stdout:len() > 0 then
      print(rv.stdout)
    end
    if rv.stderr:len() > 0 then
      print(rv.stderr)
    end
    if die_on_fail then
      die(die_msg)
    end
    return nil
  end
  return rv.stdout
end

--- Run a command, return nil on failure
--- @param cmd string[]
--- @param stdin string?
---
--- @return string?
local function run(cmd, stdin)
  return _run(cmd, false, '', stdin)
end

--- Run a command, die on failure with err_msg
--- @param cmd string[]
--- @param err_msg string
--- @param stdin string?
---
--- @return string
local function run_die(cmd, err_msg, stdin)
  return assert(_run(cmd, true, err_msg, stdin))
end

--- MIME-decode if python3 is available, else returns the input unchanged.
local function mime_decode(encoded)
  local has_python = vim.system({ 'python3', '--version' }, { text = true }):wait()
  if has_python.code ~= 0 then
    return encoded
  end

  local pycode = string.format(
    vim.text.indent(
      0,
      [[
      import sys
      from email.header import decode_header
      inp = %q
      parts = []
      for txt, cs in decode_header(inp):
          if isinstance(txt, bytes):
              try:
                  parts.append(txt.decode(cs or "utf-8", errors="replace"))
              except Exception:
                  parts.append(txt.decode("utf-8", errors="replace"))
          else:
              parts.append(txt)
      sys.stdout.write("".join(parts))
      ]]
    ),
    encoded
  )

  local result = vim.system({ 'python3', '-c', pycode }, { text = true }):wait()

  if result.code ~= 0 or not result.stdout then
    return encoded
  end

  -- Trim trailing newline Python prints only if program prints it
  return vim.trim(result.stdout)
end

local function get_commit_msg(close_pr_lines, co_author_lines)
  return ('docs: misc\n\n%s\n\n%s\n'):format(
    table.concat(close_pr_lines, '\n'),
    table.concat(co_author_lines, '\n')
  )
end

local function get_fail_msg(msg, pr_number, close_pr_lines, co_author_lines)
  return ('%s %s\n\nPending commit message:\n%s'):format(
    msg,
    pr_number or '',
    get_commit_msg(close_pr_lines, co_author_lines)
  )
end

local function main()
  local pr_list = vim.json.decode(
    run_die(
      { 'gh', 'pr', 'list', '--label', 'typo', '--json', 'number' },
      'Failed to get list of typo PRs'
    )
  )
  --- @type integer[]
  local pr_numbers = vim
    .iter(pr_list)
    :map(function(pr)
      return pr.number
    end)
    :totable()
  table.sort(pr_numbers)

  local close_pr_lines = {}
  local co_author_lines = {}
  for _, pr_number in ipairs(pr_numbers) do
    print(('PR #%s'):format(pr_number))
    local patch_file = run_die(
      { 'gh', 'pr', 'diff', tostring(pr_number), '--patch' },
      get_fail_msg('Failed to get patch for PR', pr_number, close_pr_lines, co_author_lines)
    )
    -- Using --3way allows skipping changes already included in a previous commit.
    -- If there are conflicts, it will fail and need manual conflict resolution.
    if run({ 'git', 'apply', '--index', '--3way', '-' }, patch_file) then
      table.insert(close_pr_lines, ('Close #%d'):format(pr_number))
      for author in patch_file:gmatch('\nFrom: (.- <.->)\n') do
        local co_author_line = ('Co-authored-by: %s'):format(mime_decode(author))
        if not vim.list_contains(co_author_lines, co_author_line) then
          table.insert(co_author_lines, co_author_line)
        end
      end
      for author in patch_file:gmatch('\nCo%-authored%-by: (.- <.->)\n') do
        local co_author_line = ('Co-authored-by: %s'):format(mime_decode(author))
        if not vim.list_contains(co_author_lines, co_author_line) then
          table.insert(co_author_lines, co_author_line)
        end
      end
    else
      print(
        get_fail_msg('Failed to apply patch for PR', pr_number, close_pr_lines, co_author_lines)
      )
    end
  end

  local msg = get_commit_msg(close_pr_lines, co_author_lines)
  print(
    run_die(
      { 'git', 'commit', '--file', '-' },
      get_fail_msg('Failed to create commit', nil, close_pr_lines, co_author_lines),
      msg
    )
  )
end

main()