bump_deps.lua 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. -- Usage:
  2. -- # bump to version
  3. -- nvim -es +"lua require('scripts.bump_deps').version(dependency, version_tag)"
  4. --
  5. -- # bump to commit
  6. -- nvim -es +"lua require('scripts.bump_deps').commit(dependency, commit_hash)"
  7. --
  8. -- # bump to HEAD
  9. -- nvim -es +"lua require('scripts.bump_deps').head(dependency)"
  10. --
  11. -- # submit PR
  12. -- nvim -es +"lua require('scripts.bump_deps').submit_pr()"
  13. --
  14. -- # create branch
  15. -- nvim -es +"lua require('scripts.bump_deps').create_branch()"
  16. local M = {}
  17. local _trace = false
  18. local required_branch_prefix = "bump-"
  19. local commit_prefix = "build(deps): "
  20. -- Print message
  21. local function p(s)
  22. vim.cmd("set verbose=1")
  23. vim.api.nvim_echo({ { s, "" } }, false, {})
  24. vim.cmd("set verbose=0")
  25. end
  26. local function die()
  27. p("")
  28. vim.cmd("cquit 1")
  29. end
  30. -- Executes and returns the output of `cmd`, or nil on failure.
  31. -- if die_on_fail is true, process dies with die_msg on failure
  32. --
  33. -- Prints `cmd` if `trace` is enabled.
  34. local function _run(cmd, die_on_fail, die_msg)
  35. if _trace then
  36. p("run: " .. vim.inspect(cmd))
  37. end
  38. local rv = vim.trim(vim.fn.system(cmd)) or ""
  39. if vim.v.shell_error ~= 0 then
  40. if die_on_fail then
  41. if _trace then
  42. p(rv)
  43. end
  44. p(die_msg)
  45. die()
  46. end
  47. return nil
  48. end
  49. return rv
  50. end
  51. -- Run a command, return nil on failure
  52. local function run(cmd)
  53. return _run(cmd, false, "")
  54. end
  55. -- Run a command, die on failure with err_msg
  56. local function run_die(cmd, err_msg)
  57. return _run(cmd, true, err_msg)
  58. end
  59. local function require_executable(cmd)
  60. local cmd_path = run_die({ "command", "-v", cmd }, cmd .. " not found!")
  61. run_die({ "test", "-x", cmd_path }, cmd .. " is not executable")
  62. end
  63. local function rm_file_if_present(path_to_file)
  64. run({ "rm", "-f", path_to_file })
  65. end
  66. local nvim_src_dir = vim.fn.getcwd()
  67. local temp_dir = nvim_src_dir .. "/tmp"
  68. run({ "mkdir", "-p", temp_dir })
  69. local function get_dependency(dependency_name)
  70. local dependency_table = {
  71. ["LuaJIT"] = {
  72. repo = "LuaJIT/LuaJIT",
  73. symbol = "LUAJIT",
  74. },
  75. ["libuv"] = {
  76. repo = "libuv/libuv",
  77. symbol = "LIBUV",
  78. },
  79. ["Luv"] = {
  80. repo = "luvit/luv",
  81. symbol = "LUV",
  82. },
  83. ["tree-sitter"] = {
  84. repo = "tree-sitter/tree-sitter",
  85. symbol = "TREESITTER",
  86. },
  87. }
  88. local dependency = dependency_table[dependency_name]
  89. if dependency == nil then
  90. p("Not a dependency: " .. dependency_name)
  91. die()
  92. end
  93. dependency.name = dependency_name
  94. return dependency
  95. end
  96. local function get_gh_commit_sha(repo, ref)
  97. require_executable("gh")
  98. local sha = run_die(
  99. { "gh", "api", "repos/" .. repo .. "/commits/" .. ref, "--jq", ".sha" },
  100. "Failed to get commit hash from GitHub. Not a valid ref?"
  101. )
  102. return sha
  103. end
  104. local function get_archive_info(repo, ref)
  105. require_executable("curl")
  106. local archive_name = ref .. ".tar.gz"
  107. local archive_path = temp_dir .. "/" .. archive_name
  108. local archive_url = "https://github.com/" .. repo .. "/archive/" .. archive_name
  109. rm_file_if_present(archive_path)
  110. run_die({ "curl", "-sL", archive_url, "-o", archive_path }, "Failed to download archive from GitHub")
  111. local archive_sha = run({ "sha256sum", archive_path }):gmatch("%w+")()
  112. return { url = archive_url, sha = archive_sha }
  113. end
  114. local function write_cmakelists_line(symbol, kind, value)
  115. require_executable("sed")
  116. local cmakelists_path = nvim_src_dir .. "/" .. "third-party/CMakeLists.txt"
  117. run_die({
  118. "sed",
  119. "-i",
  120. "-e",
  121. "s/set(" .. symbol .. "_" .. kind .. ".*$" .. "/set(" .. symbol .. "_" .. kind .. " " .. value .. ")" .. "/",
  122. cmakelists_path,
  123. }, "Failed to write " .. cmakelists_path)
  124. end
  125. local function explicit_create_branch(dep)
  126. require_executable("git")
  127. local checked_out_branch = run({ "git", "rev-parse", "--abbrev-ref", "HEAD" })
  128. if checked_out_branch ~= "master" then
  129. p("Not on master!")
  130. die()
  131. end
  132. run_die({ "git", "checkout", "-b", "bump-" .. dep }, "git failed to create branch")
  133. end
  134. local function verify_branch(new_branch_suffix)
  135. require_executable("git")
  136. local checked_out_branch = run({ "git", "rev-parse", "--abbrev-ref", "HEAD" })
  137. if not checked_out_branch:match("^" .. required_branch_prefix) then
  138. p("Current branch '" .. checked_out_branch .. "' doesn't seem to start with " .. required_branch_prefix)
  139. p("Checking out to bump-" .. new_branch_suffix)
  140. explicit_create_branch(new_branch_suffix)
  141. end
  142. end
  143. local function update_cmakelists(dependency, archive, comment)
  144. require_executable("git")
  145. verify_branch(dependency.name)
  146. local changed_file = nvim_src_dir .. "/" .. "third-party/CMakeLists.txt"
  147. p("Updating " .. dependency.name .. " to " .. archive.url .. "\n")
  148. write_cmakelists_line(dependency.symbol, "URL", archive.url:gsub("/", "\\/"))
  149. write_cmakelists_line(dependency.symbol, "SHA256", archive.sha)
  150. run_die(
  151. { "git", "commit", changed_file, "-m", commit_prefix .. "bump " .. dependency.name .. " to " .. comment },
  152. "git failed to commit"
  153. )
  154. end
  155. local function verify_cmakelists_committed()
  156. require_executable("git")
  157. local cmakelists_path = nvim_src_dir .. "/" .. "third-party/CMakeLists.txt"
  158. run_die({ "git", "diff", "--quiet", "HEAD", "--", cmakelists_path }, cmakelists_path .. " has uncommitted changes")
  159. end
  160. local function warn_luv_symbol()
  161. p("warning: " .. get_dependency("Luv").symbol .. "_VERSION will not be updated")
  162. end
  163. -- return first 9 chars of commit
  164. local function short_commit(commit)
  165. return string.sub(commit, 1, 9)
  166. end
  167. -- TODO: remove hardcoded fork
  168. local function gh_pr(pr_title, pr_body)
  169. require_executable("gh")
  170. local pr_url = run_die({
  171. "gh",
  172. "pr",
  173. "create",
  174. "--title",
  175. pr_title,
  176. "--body",
  177. pr_body,
  178. }, "Failed to create PR")
  179. return pr_url
  180. end
  181. local function find_git_remote(fork)
  182. require_executable("git")
  183. local remotes = run({ "git", "remote", "-v" })
  184. local git_remote = ""
  185. for remote in remotes:gmatch("[^\r\n]+") do
  186. local words = {}
  187. for word in remote:gmatch("%w+") do
  188. table.insert(words, word)
  189. end
  190. local match = words[1]:match("/github.com[:/]neovim/neovim/")
  191. if fork == "fork" then
  192. match = not match
  193. end
  194. if match and words[3] == "(fetch)" then
  195. git_remote = words[0]
  196. break
  197. end
  198. end
  199. if git_remote == "" then
  200. git_remote = "origin"
  201. end
  202. return git_remote
  203. end
  204. local function create_pr(pr_title, pr_body)
  205. require_executable("git")
  206. local push_first = true
  207. local checked_out_branch = run({ "git", "rev-parse", "--abbrev-ref", "HEAD" })
  208. if push_first then
  209. local push_remote = run({ "git", "config", "--get", "branch." .. checked_out_branch .. ".pushRemote" })
  210. if push_remote == nil then
  211. push_remote = run({ "git", "config", "--get", "remote.pushDefault" })
  212. if push_remote == nil then
  213. push_remote = run({ "git", "config", "--get", "branch." .. checked_out_branch .. ".remote" })
  214. if push_remote == nil or push_remote == find_git_remote(nil) then
  215. push_remote = find_git_remote("fork")
  216. end
  217. end
  218. end
  219. p("Pushing to " .. push_remote .. "/" .. checked_out_branch)
  220. run_die({ "git", "push", push_remote, checked_out_branch }, "Git failed to push")
  221. end
  222. local pr_url = gh_pr(pr_title, pr_body)
  223. p("\nCreated PR: " .. pr_url .. "\n")
  224. end
  225. function M.commit(dependency_name, commit)
  226. local dependency = get_dependency(dependency_name)
  227. verify_cmakelists_committed()
  228. local commit_sha = get_gh_commit_sha(dependency.repo, commit)
  229. if commit_sha ~= commit then
  230. p("Not a commit: " .. commit .. ". Did you mean version?")
  231. die()
  232. end
  233. local archive = get_archive_info(dependency.repo, commit)
  234. if dependency_name == "Luv" then
  235. warn_luv_symbol()
  236. end
  237. update_cmakelists(dependency, archive, short_commit(commit))
  238. end
  239. function M.version(dependency_name, version)
  240. local dependency = get_dependency(dependency_name)
  241. verify_cmakelists_committed()
  242. local commit_sha = get_gh_commit_sha(dependency.repo, version)
  243. if commit_sha == version then
  244. p("Not a version: " .. version .. ". Did you mean commit?")
  245. die()
  246. end
  247. local archive = get_archive_info(dependency.repo, version)
  248. if dependency_name == "Luv" then
  249. write_cmakelists_line(dependency.symbol, "VERSION", version)
  250. end
  251. update_cmakelists(dependency, archive, version)
  252. end
  253. function M.head(dependency_name)
  254. local dependency = get_dependency(dependency_name)
  255. verify_cmakelists_committed()
  256. local commit_sha = get_gh_commit_sha(dependency.repo, "HEAD")
  257. local archive = get_archive_info(dependency.repo, commit_sha)
  258. if dependency_name == "Luv" then
  259. warn_luv_symbol()
  260. end
  261. update_cmakelists(dependency, archive, "HEAD - " .. short_commit(commit_sha))
  262. end
  263. function M.create_branch(dep)
  264. explicit_create_branch(dep)
  265. end
  266. function M.submit_pr()
  267. require_executable("git")
  268. verify_branch("deps")
  269. local nvim_remote = find_git_remote(nil)
  270. local relevant_commit = run_die({
  271. "git",
  272. "log",
  273. "--grep=" .. commit_prefix,
  274. "--reverse",
  275. "--format='%s'",
  276. nvim_remote .. "/master..HEAD",
  277. "-1",
  278. }, "Failed to fetch commits")
  279. local pr_title
  280. local pr_body
  281. if relevant_commit == "" then
  282. pr_title = commit_prefix .. "bump some dependencies"
  283. pr_body = "bump some dependencies"
  284. else
  285. relevant_commit = relevant_commit:gsub("'", "")
  286. pr_title = relevant_commit
  287. pr_body = relevant_commit:gsub(commit_prefix:gsub("%(", "%%("):gsub("%)", "%%)"), "")
  288. end
  289. pr_body = pr_body .. "\n\n(add explanations if needed)"
  290. p(pr_title .. "\n" .. pr_body .. "\n")
  291. create_pr(pr_title, pr_body)
  292. end
  293. return M