diff --git a/lua/octo/commands.lua b/lua/octo/commands.lua index f4745432d..ecac7ea90 100644 --- a/lua/octo/commands.lua +++ b/lua/octo/commands.lua @@ -13,6 +13,11 @@ local vim = vim local M = {} +local get_current_buffer = function() + local bufnr = vim.api.nvim_get_current_buf() + return octo_buffers[bufnr] +end + function M.setup() vim.api.nvim_create_user_command("Octo", function(opts) require("octo.commands").octo(unpack(opts.fargs)) @@ -58,6 +63,48 @@ function M.setup() picker.discussions(opts) end, }, + milestone = { + list = function(repo, ...) + local opts = M.process_varargs(repo, ...) + opts.cb = function(item) + utils.info("Picked " .. item.title) + end + picker.milestones(opts) + end, + add = function(repo, ...) + local buffer = get_current_buffer() + if not buffer then + return + end + + local opts = M.process_varargs(repo, ...) + opts.cb = function(item) + utils.add_milestone(buffer:isIssue(), buffer.number, item.title) + end + picker.milestones(opts) + end, + remove = function(repo, ...) + local buffer = get_current_buffer() + if not buffer then + return + end + + local milestone = buffer.node.milestone + if utils.is_blank(milestone) then + utils.error "No milestone to remove" + return + end + + utils.remove_milestone(buffer:isIssue(), buffer.number) + end, + create = function(...) + vim.fn.inputsave() + local title = vim.fn.input "Enter milestone title: " + local description = vim.fn.input "Enter milestone description: " + vim.fn.inputrestore() + utils.create_milestone(title, description) + end, + }, issue = { create = function(repo) M.create_issue(repo) diff --git a/lua/octo/gh/graphql.lua b/lua/octo/gh/graphql.lua index 6a5edac55..6c3d55f09 100644 --- a/lua/octo/gh/graphql.lua +++ b/lua/octo/gh/graphql.lua @@ -3548,6 +3548,20 @@ query { } ]] +M.open_milestones_query = [[ +query($name: String!, $owner: String!, $n_milestones: Int!) { + repository(owner: $owner, name: $name) { + milestones(first: $n_milestones, states: [OPEN]) { + nodes { + id + title + description + } + } + } +} +]] + return function(query, ...) local opts = { escape = true } for _, v in ipairs { ... } do diff --git a/lua/octo/pickers/fzf-lua/provider.lua b/lua/octo/pickers/fzf-lua/provider.lua index e78150914..b96ca4444 100644 --- a/lua/octo/pickers/fzf-lua/provider.lua +++ b/lua/octo/pickers/fzf-lua/provider.lua @@ -27,6 +27,7 @@ M.picker = { review_commits = require "octo.pickers.fzf-lua.pickers.review_commits", search = require "octo.pickers.fzf-lua.pickers.search", users = require "octo.pickers.fzf-lua.pickers.users", + milestones = M.not_implemented, } return M diff --git a/lua/octo/pickers/telescope/entry_maker.lua b/lua/octo/pickers/telescope/entry_maker.lua index c28b84d4e..17452b0c6 100644 --- a/lua/octo/pickers/telescope/entry_maker.lua +++ b/lua/octo/pickers/telescope/entry_maker.lua @@ -340,6 +340,50 @@ function M.gen_from_project_card() end end +function M.gen_from_milestone(title_width, show_description) + title_width = title_width or 10 + + local make_display = function(entry) + if not entry then + return nil + end + + local columns, items + if show_description then + columns = { + { entry.milestone.title, "OctoDetailsLabel" }, + { entry.milestone.description }, + } + items = { { width = title_width }, { remaining = true } } + else + columns = { + { entry.milestone.title, "OctoDetailsLabel" }, + } + items = { { width = title_width } } + end + + local displayer = entry_display.create { + separator = "", + items = items, + } + + return displayer(columns) + end + + return function(milestone) + if not milestone or vim.tbl_isempty(milestone) then + return nil + end + + return { + value = milestone.id, + ordinal = milestone.title, + display = make_display, + milestone = milestone, + } + end +end + function M.gen_from_label() local make_display = function(entry) if not entry then diff --git a/lua/octo/pickers/telescope/provider.lua b/lua/octo/pickers/telescope/provider.lua index f5e5c4b38..f56f9d166 100644 --- a/lua/octo/pickers/telescope/provider.lua +++ b/lua/octo/pickers/telescope/provider.lua @@ -756,6 +756,10 @@ local function select(opts) local selection = action_state.get_selected_entry(prompt_bufnr) table.insert(items, get_item(selection)) cb = single_cb + elseif multiple_cb == nil then + utils.error "Multiple selections are not allowed" + actions.close(prompt_bufnr) + return else for _, selection in ipairs(selections) do table.insert(items, get_item(selection)) @@ -1268,6 +1272,74 @@ function M.discussions(opts) } end +function M.milestones(opts) + local owner, name = utils.split_repo(opts.repo) + local query = graphql "open_milestones_query" + + gh.graphql { + query = query, + fields = { + owner = owner, + name = name, + n_milestones = 25, + }, + opts = { + cb = function(output, stderr) + if stderr and not utils.is_blank(stderr) then + utils.error(stderr) + return + end + + local resp = vim.fn.json_decode(output) + local nodes = resp.data.repository.milestones.nodes + + if #nodes == 0 then + utils.error(string.format("There are no matching milestones in %s.", opts.repo)) + return + end + + local title_width = 0 + for _, milestone in ipairs(nodes) do + title_width = math.max(title_width, #milestone.title) + end + + local non_empty_descriptions = false + for _, milestone in ipairs(nodes) do + if not utils.is_blank(milestone.description) then + non_empty_descriptions = true + break + end + end + + pickers + .new(vim.deepcopy(dropdown_opts), { + finder = finders.new_table { + results = nodes, + entry_maker = entry_maker.gen_from_milestone(title_width, non_empty_descriptions), + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(_, map) + actions.select_default:replace(function(prompt_bufnr) + select { + bufnr = prompt_bufnr, + single_cb = function(selected) + opts.cb(selected[1]) + end, + multiple_cb = nil, + get_item = function(selected) + return selected.milestone + end, + } + end) + return true + end, + }) + :find() + end, + }, + } +end + M.picker = { actions = M.actions, assigned_labels = M.select_assigned_label, @@ -1289,6 +1361,7 @@ M.picker = { review_commits = M.review_commits, search = M.search, users = M.select_user, + milestones = M.milestones, } return M diff --git a/lua/octo/pickers/vim-clap/provider.lua b/lua/octo/pickers/vim-clap/provider.lua index aaa731ec5..7f2d7ea36 100644 --- a/lua/octo/pickers/vim-clap/provider.lua +++ b/lua/octo/pickers/vim-clap/provider.lua @@ -27,6 +27,7 @@ M.picker = { review_commits = M.not_implemented, search = M.not_implemented, users = M.not_implemented, + milestones = M.not_implemented, } return M diff --git a/lua/octo/utils.lua b/lua/octo/utils.lua index b5f5a81fe..0acc5f7f7 100644 --- a/lua/octo/utils.lua +++ b/lua/octo/utils.lua @@ -321,6 +321,76 @@ function M.commit_exists(commit, cb) }):start() end +---Add a milestone to an issue or PR +function M.add_milestone(issue, number, milestone_name) + local command = issue and "issue" or "pr" + local args = { command, "edit", number, "--milestone", milestone_name } + + gh.run { + args = args, + cb = function(output, stderr) + if stderr and not M.is_blank(stderr) then + M.error(stderr) + elseif output then + M.info("Added milestone " .. milestone_name) + end + end, + } +end + +---Remove a milestone from an issue or PR +function M.remove_milestone(issue, number) + local command = issue and "issue" or "pr" + local args = { command, "edit", number, "--remove-milestone" } + + gh.run { + args = args, + cb = function(output, stderr) + if stderr and not M.is_blank(stderr) then + M.error(stderr) + elseif output then + M.info "Removed milestone" + end + end, + } +end + +---https://docs.github.com/en/rest/issues/milestones?apiVersion=2022-11-28#create-a-milestone +---Create a new milestone +function M.create_milestone(title, description) + if M.is_blank(title) then + M.error "Title is required to create milestone" + return + end + + local owner, name = M.split_repo(M.get_remote_name()) + local endpoint = string.format("repos/%s/%s/milestones", owner, name) + local args = { "api", "--method", "POST", endpoint } + + local data = { + title = title, + description = description, + state = "open", + } + + for key, value in pairs(data) do + table.insert(args, "-f") + table.insert(args, string.format("%s=%s", key, value)) + end + + gh.run { + args = args, + cb = function(output, stderr) + if stderr and not M.is_blank(stderr) then + M.error(stderr) + elseif output then + local resp = vim.fn.json_decode(output) + M.info("Created milestone " .. resp.title) + end + end, + } +end + function M.develop_issue(issue_repo, issue_number, branch_repo) if M.is_blank(branch_repo) then branch_repo = M.get_remote_name()