diff --git a/README.md b/README.md index 6896ea49..53dc7608 100644 --- a/README.md +++ b/README.md @@ -408,6 +408,8 @@ If no command is passed, the argument to `Octo` is treated as a URL from where a | search | | Search GitHub for issues and PRs matching the [query](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests) | | run | list | List workflow runs | | notification | list | Shows current unread notifications | +| discussion | list [repo] | List open discussions for current or specified repo | +| | create [repo] | Create discussion for current or specified repo | 0. `[repo]`: If repo is not provided, it will be derived from `/.git/config`. diff --git a/doc/octo.txt b/doc/octo.txt index dba3bb2c..de9107ca 100644 --- a/doc/octo.txt +++ b/doc/octo.txt @@ -81,6 +81,14 @@ See |octo-command-examples| for examples. system clipboard. +:Octo discussion [action] {args} *octo-commands-discussion* + + list [repo] Lists all open discussions for the current or specified repo. + In-menu mappings: + Copy URL to system clipboard. + create [repo] Creates a new discussion for the current or specified repo. + + :Octo repo [action] *octo-commands-repo* list Lists repos the user owns, contributes, or belongs to. diff --git a/lua/octo/commands.lua b/lua/octo/commands.lua index 5c552058..57aeca63 100644 --- a/lua/octo/commands.lua +++ b/lua/octo/commands.lua @@ -95,6 +95,16 @@ function M.setup() local opts = M.process_varargs(repo, ...) picker.discussions(opts) end, + create = function(repo, ...) + local opts = M.process_varargs(repo, ...) + + if not opts.repo then + utils.error "No repo found" + return + end + + require("octo.discussions").create(opts) + end, }, milestone = { list = function(repo, ...) diff --git a/lua/octo/discussions.lua b/lua/octo/discussions.lua new file mode 100644 index 00000000..2c041806 --- /dev/null +++ b/lua/octo/discussions.lua @@ -0,0 +1,119 @@ +--- Helpers for discussions +local gh = require "octo.gh" +local graphql = require "octo.gh.graphql" +local utils = require "octo.utils" + +local M = {} + +---@class DiscussionMutationOpts +---@field repo_id string +---@field category_id string +---@field title string +---@field body string + +--- Discussion mutation +---@param opts DiscussionMutationOpts +local create_discussion = function(opts) + gh.api.graphql { + query = graphql "create_discussion_mutation", + fields = { + repo_id = opts.repo_id, + category_id = opts.category_id, + title = opts.title, + body = opts.body, + }, + jq = ".data.createDiscussion.discussion", + opts = { + cb = gh.create_callback { + success = function(output) + utils.info("Successfully created discussion " .. opts.title) + local resp = vim.json.decode(output) + utils.copy_url(resp.url) + end, + }, + }, + } +end + +---@class Category +---@field id string +---@field name string +---@field emoji string + +---Select a category +---@param categories Category[] +---@param cb fun(selected: Category) +local select_a_category = function(categories, cb) + vim.ui.select(categories, { + prompt = "Pick a category: ", + format_item = function(item) + return item.name + end, + }, function(selected) + if selected == nil then + return + end + cb(selected) + end) +end + +---@class GetCategoriesOpts +---@field owner string +---@field name string + +---Get categories for a repository +---@param opts GetCategoriesOpts +---@param cb fun(selected: Category) +local get_categories = function(opts, cb) + gh.api.graphql { + query = graphql "discussion_categories_query", + jq = ".data.repository.discussionCategories.nodes", + fields = { owner = opts.owner, name = opts.name }, + opts = { + cb = gh.create_callback { + success = function(data) + local categories = vim.json.decode(data) + select_a_category(categories, cb) + end, + }, + }, + } +end + +---@class DiscussionOpts +---@field repo string +---@field title string|nil +---@field body string|nil + +---Create a discussion for a repository +---@param opts DiscussionOpts +---@return nil +M.create = function(opts) + opts = opts or {} + + opts.owner, opts.name = utils.split_repo(opts.repo) + local repo_info = utils.get_repo_info(opts.repo) + + if not repo_info.hasDiscussionsEnabled then + utils.error(opts.repo .. " doesn't have discussions enabled") + return + end + + opts.repo_id = repo_info.id + + if not opts.title then + opts.title = utils.input { prompt = "Creating discussion for " .. opts.repo .. ". Enter title" } + end + if not opts.body then + opts.body = utils.input { prompt = "Discussion body" } + end + + local cb = function(selected) + opts.category_id = selected.id + + create_discussion(opts) + end + get_categories(opts, cb) +end + +return M diff --git a/lua/octo/gh/graphql.lua b/lua/octo/gh/graphql.lua index 44e2eb9d..df0e66c0 100644 --- a/lua/octo/gh/graphql.lua +++ b/lua/octo/gh/graphql.lua @@ -1075,6 +1075,36 @@ query( } ]] .. fragments.discussion_info +M.discussion_categories_query = [[ +query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + discussionCategories(first: 20) { + nodes { + id + name + emoji + } + } + } +} +]] + +M.create_discussion_mutation = [[ +mutation($repo_id: ID!, $category_id: ID!, $title: String!, $body: String!) { + createDiscussion(input: { + repositoryId: $repo_id, + categoryId: $category_id, + title: $title, + body: $body + }) { + discussion { + id + url + } + } +} +]] + M.discussion_query = [[ query($owner: String!, $name: String!, $number: Int!, $endCursor: String) { repository(owner: $owner, name: $name) { @@ -1701,6 +1731,7 @@ query($owner: String!, $name: String!) { isMirror mirrorUrl hasProjectsEnabled + hasDiscussionsEnabled projectsUrl homepageUrl primaryLanguage { diff --git a/lua/octo/utils.lua b/lua/octo/utils.lua index 904e90fd..aab10873 100644 --- a/lua/octo/utils.lua +++ b/lua/octo/utils.lua @@ -727,9 +727,8 @@ function M.get_repo_info(repo) end local owner, name = M.split_repo(repo) - local query = graphql "repository_query" local output = gh.api.graphql { - query = query, + query = graphql "repository_query", fields = { owner = owner, name = name }, jq = ".data.repository", opts = { mode = "sync" }, @@ -1796,4 +1795,19 @@ function M.get_icon(entry) return M.icons.unknown end +-- +M.copy_url = function(url, register) + register = register or "+" + vim.fn.setreg(register, url, "c") + M.info("Copied '" .. url .. "' to the system clipboard (+ register)") +end + +M.input = function(opts) + vim.fn.inputsave() + local value = vim.fn.input(opts) + vim.fn.inputrestore() + + return value +end + return M