From 842e5ee03d9d9ca0013fa1d98453ad37c42098b4 Mon Sep 17 00:00:00 2001 From: Milo Gertjejansen Date: Sat, 13 Jan 2024 00:07:51 -0600 Subject: [PATCH] feat: projects v2, take 2 --- README.md | 15 +- doc/octo.txt | 18 +- lua/octo/commands.lua | 111 +++++++++- lua/octo/completion.lua | 6 +- lua/octo/config.lua | 13 ++ lua/octo/gh/fragments.lua | 28 +++ lua/octo/gh/graphql.lua | 195 +++++++++++++++--- lua/octo/gh/init.lua | 29 +++ lua/octo/init.lua | 17 +- lua/octo/navigation.lua | 2 + lua/octo/pickers/fzf-lua/entry_maker.lua | 32 +++ .../pickers/fzf-lua/pickers/project_cards.lua | 2 +- .../fzf-lua/pickers/project_cards_v2.lua | 53 +++++ .../fzf-lua/pickers/project_columns_v2.lua | 107 ++++++++++ lua/octo/pickers/fzf-lua/pickers/prs.lua | 8 +- lua/octo/pickers/fzf-lua/pickers/search.lua | 1 + lua/octo/pickers/fzf-lua/previewers.lua | 1 - lua/octo/pickers/fzf-lua/provider.lua | 2 + lua/octo/pickers/telescope/provider.lua | 6 + lua/octo/pickers/vim-clap/provider.lua | 2 + lua/octo/ui/writers.lua | 35 ++++ lua/tests/octo/config_spec.lua | 10 + 22 files changed, 649 insertions(+), 44 deletions(-) create mode 100644 lua/octo/gh/fragments.lua create mode 100644 lua/octo/pickers/fzf-lua/pickers/project_cards_v2.lua create mode 100644 lua/octo/pickers/fzf-lua/pickers/project_columns_v2.lua diff --git a/README.md b/README.md index 2e0ae470..ca42bee2 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,15 @@ Edit and review GitHub issues and pull requests from the comfort of your favorit ## 🎯 Requirements - Install [GitHub CLI](https://cli.github.com/) + - If you'd like to use [Projects v2](https://docs.github.com/en/issues/planning-and-tracking-with-projects) + you will need to add the `read:project` scope to your `gh` token. You can + do so by running `gh auth refresh -s read:project`. + - If you'd like to actually modify projects you can instead add the `project` + scope to your token instead. - Install [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) -- Install [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) +- Install one of: + - [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) + - [fzf-lua](https://github.com/ibhagwan/fzf-lua) - Install [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) ## 📦 Installation @@ -73,6 +80,7 @@ use { requires = { 'nvim-lua/plenary.nvim', 'nvim-telescope/telescope.nvim', + -- OR 'ibhagwan/fzf-lua', 'nvim-tree/nvim-web-devicons', }, config = function () @@ -112,9 +120,14 @@ require"octo".setup({ snippet_context_lines = 4; -- number or lines around commented lines gh_env = {}, -- extra environment variables to pass on to GitHub CLI, can be a table or function returning a table timeout = 5000, -- timeout for requests between the remote server + default_to_projects_v2 = false, -- use projects v2 for the `Octo card ...` command by default. Both legacy and v2 commands are available under `Octo cardlegacy ...` and `Octo cardv2 ...` respectively. ui = { use_signcolumn = true, -- show "modified" marks on the sign column }, + picker = "telescope", -- "telescope" | "fzf-lua" + picker_config = { + use_emojis = false, -- Only used in fzf-lua picker. If you want emojis when viewing the picker set to true. + }, issues = { order_by = { -- criteria to sort results of `Octo issue list` field = "CREATED_AT", -- either COMMENTS, CREATED_AT or UPDATED_AT (https://docs.github.com/en/graphql/reference/enums#issueorderfield) diff --git a/doc/octo.txt b/doc/octo.txt index 515f9764..b9155775 100644 --- a/doc/octo.txt +++ b/doc/octo.txt @@ -326,7 +326,23 @@ Can I use treesitter markdown parser with octo buffers?~ >lua vim.keymap.set("i", "@", "@", { silent = true, buffer = true }) vim.keymap.set("i", "#", "#", { silent = true, buffer = true }) -< + + +I can't see my v2 projects in issues and/or pull requests!~ + + (and I see a warning when I open them) + + You are missing a scope from the token gh uses. You can add the scope to + your gh token with + + `gh auth refresh -s read:project` + + Alternatively if you want to be able to modify projects (i.e. add/remove + cards) you need to add the `project` scope to your token instead. + + If you don't care about projects v2 you can suppress the warning by setting + `suppress_missing_scope.project_v2 = true` in your Octo config. + CREDITS *octo-credits* diff --git a/lua/octo/commands.lua b/lua/octo/commands.lua index 72afa1ca..4d3ad3c1 100644 --- a/lua/octo/commands.lua +++ b/lua/octo/commands.lua @@ -17,6 +17,32 @@ function M.setup() vim.api.nvim_create_user_command("Octo", function(opts) require("octo.commands").octo(unpack(opts.fargs)) end, { complete = require("octo.completion").octo_command_complete, nargs = "*" }) + local conf = config.values + + local card_commands + + if conf.default_to_projects_v2 then + card_commands = { + set = function() + M.set_project_v2_card() + end, + remove = function() + M.remove_project_v2_card() + end, + } + else + card_commands = { + add = function() + M.add_project_card() + end, + move = function() + M.move_project_card() + end, + remove = function() + M.remove_project_card() + end, + } + end -- supported commands M.commands = { @@ -295,7 +321,16 @@ function M.setup() M.reaction_action "HEART" end, }, - card = { + card = card_commands, + cardv2 = { + set = function(...) + M.set_project_v2_card() + end, + remove = function() + M.remove_project_v2_card() + end, + }, + cardlegacy = { add = function() M.add_project_card() end, @@ -1291,6 +1326,80 @@ function M.move_project_card() end) end +function M.set_project_v2_card() + local bufnr = vim.api.nvim_get_current_buf() + local buffer = octo_buffers[bufnr] + if not buffer then + return + end + + -- show column selection picker + picker.project_columns_v2(function(project_id, field_id, value) + -- add new card + local add_query = graphql("add_project_v2_item_mutation", buffer.node.id, project_id) + gh.run { + args = { "api", "graphql", "--paginate", "-f", string.format("query=%s", add_query) }, + cb = function(add_output, add_stderr) + if add_stderr and not utils.is_blank(add_stderr) then + utils.error(add_stderr) + elseif add_output then + local resp = vim.fn.json_decode(add_output) + local update_query = graphql( + "update_project_v2_item_mutation", + project_id, + resp.data.addProjectV2ItemById.item.id, + field_id, + value + ) + gh.run { + args = { "api", "graphql", "--paginate", "-f", string.format("query=%s", update_query) }, + cb = function(update_output, update_stderr) + if update_stderr and not utils.is_blank(update_stderr) then + utils.error(update_stderr) + elseif update_output then + -- TODO do update here + -- refresh issue/pr details + require("octo").load(buffer.repo, buffer.kind, buffer.number, function(obj) + writers.write_details(bufnr, obj, true) + buffer.node.projectCards = obj.projectCards + end) + end + end, + } + end + end, + } + end) +end + +function M.remove_project_v2_card() + local bufnr = vim.api.nvim_get_current_buf() + local buffer = octo_buffers[bufnr] + if not buffer then + return + end + + -- show card selection picker + picker.project_cards_v2(function(project_id, item_id) + -- delete card + local query = graphql("delete_project_v2_item_mutation", project_id, item_id) + gh.run { + args = { "api", "graphql", "--paginate", "-f", string.format("query=%s", query) }, + cb = function(output, stderr) + if stderr and not utils.is_blank(stderr) then + utils.error(stderr) + elseif output then + -- refresh issue/pr details + require("octo").load(buffer.repo, buffer.kind, buffer.number, function(obj) + buffer.node.projectCards = obj.projectCards + writers.write_details(bufnr, obj, true) + end) + end + end, + } + end) +end + function M.reload(bufnr) require("octo").load_buffer(bufnr) end diff --git a/lua/octo/completion.lua b/lua/octo/completion.lua index 2cb4a1f6..f5e6fdf8 100644 --- a/lua/octo/completion.lua +++ b/lua/octo/completion.lua @@ -25,10 +25,8 @@ function M.octo_command_complete(argLead, cmdLine) return get_options(command_keys) elseif #parts == 2 and vim.tbl_contains(command_keys, parts[2]) or #parts == 3 then local obj = octo_commands.commands[parts[2]] - if obj then - if type(obj) == "table" then - return get_options(vim.tbl_keys(obj)) - end + if type(obj) == "table" then + return get_options(vim.tbl_keys(obj)) end end end diff --git a/lua/octo/config.lua b/lua/octo/config.lua index deb94c76..5ec2ed06 100644 --- a/lua/octo/config.lua +++ b/lua/octo/config.lua @@ -40,6 +40,9 @@ local M = {} ---@field field string ---@field direction "ASC" | "DESC" +---@class OctoMissingScopeConfig +---@field projects_v2 boolean + ---@class OctoConfig Octo configuration settings ---@field picker OctoPickers ---@field picker_config OctoPickerConfig @@ -60,6 +63,8 @@ local M = {} ---@field snippet_context_lines number ---@field gh_env table ---@field timeout number +---@field default_to_projects_v2 boolean +---@field suppress_missing_scope OctoMissingScopeConfig ---@field ui OctoConfigUi ---@field issues OctoConfigIssues ---@field pull_requests OctoConfigPR @@ -98,6 +103,10 @@ function M.get_default_values() snippet_context_lines = 4, gh_env = {}, timeout = 5000, + default_to_projects_v2 = false, + suppress_missing_scope = { + projects_v2 = false, + }, ui = { use_signcolumn = true, }, @@ -361,6 +370,10 @@ function M.validate_config() validate_type(config.enable_builtin, "enable_builtin", "boolean") validate_type(config.snippet_context_lines, "snippet_context_lines", "number") validate_type(config.timeout, "timeout", "number") + validate_type(config.default_to_projects_v2, "default_to_projects_v2", "boolean") + if validate_type(config.suppress_missing_scope, "supress_missing_scope", "table") then + validate_type(config.suppress_missing_scope.projects_v2, "supress_missing_scope.projects_v2", "boolean") + end validate_type(config.gh_env, "gh_env", "table") validate_type(config.reaction_viewer_hint_icon, "reaction_viewer_hint_icon", "string") validate_type(config.user_icon, "user_icon", "string") diff --git a/lua/octo/gh/fragments.lua b/lua/octo/gh/fragments.lua new file mode 100644 index 00000000..7781dc41 --- /dev/null +++ b/lua/octo/gh/fragments.lua @@ -0,0 +1,28 @@ +local M = {} + +M.projects_v2_fragment = [[ + projectItems(first: 100) { + nodes { + id + project { + id + title + } + fieldValues(first: 100) { + nodes { + ... on ProjectV2ItemFieldSingleSelectValue { + name + optionId + field { + ... on ProjectV2SingleSelectField { + name + } + } + } + } + } + } + } +]] + +return M diff --git a/lua/octo/gh/graphql.lua b/lua/octo/gh/graphql.lua index e9652335..2d4e819c 100644 --- a/lua/octo/gh/graphql.lua +++ b/lua/octo/gh/graphql.lua @@ -46,7 +46,7 @@ M.resolve_review_thread_mutation = [[ path pullRequest { reviewThreads(last:100) { - nodes { + nodes { id path diffSide @@ -111,7 +111,7 @@ M.unresolve_review_thread_mutation = [[ path pullRequest { reviewThreads(last:100) { - nodes { + nodes { id path diffSide @@ -173,7 +173,7 @@ M.start_review_mutation = [[ state pullRequest { reviewThreads(last:100) { - nodes { + nodes { id path line @@ -272,8 +272,8 @@ M.submit_pull_request_review_mutation = [[ ]] M.delete_pull_request_review_mutation = [[ -mutation { - deletePullRequestReview(input: {pullRequestReviewId: "%s"}) { +mutation { + deletePullRequestReview(input: {pullRequestReviewId: "%s"}) { pullRequestReview { id state @@ -284,8 +284,8 @@ mutation { -- https://docs.github.com/en/graphql/reference/mutations#addpullrequestreviewthread M.add_pull_request_review_thread_mutation = [[ -mutation { - addPullRequestReviewThread(input: { pullRequestReviewId: "%s", body: "%s", path: "%s", side: %s, line:%d}) { +mutation { + addPullRequestReviewThread(input: { pullRequestReviewId: "%s", body: "%s", path: "%s", side: %s, line:%d}) { thread { id comments(last:100) { @@ -319,11 +319,11 @@ mutation { totalCount } } - } + } } pullRequest { reviewThreads(last:100) { - nodes { + nodes { id path diffSide @@ -378,8 +378,8 @@ mutation { -- https://docs.github.com/en/graphql/reference/mutations#addpullrequestreviewthread M.add_pull_request_review_multiline_thread_mutation = [[ -mutation { - addPullRequestReviewThread(input: { pullRequestReviewId: "%s", body: "%s", path: "%s", startSide: %s, side: %s, startLine: %d, line:%d}) { +mutation { + addPullRequestReviewThread(input: { pullRequestReviewId: "%s", body: "%s", path: "%s", startSide: %s, side: %s, startLine: %d, line:%d}) { thread { id comments(last:100) { @@ -413,11 +413,11 @@ mutation { totalCount } } - } + } } pullRequest { reviewThreads(last:100) { - nodes { + nodes { id path diffSide @@ -505,7 +505,7 @@ M.update_pull_request_review_comment_mutation = [[ body pullRequest { reviewThreads(last:100) { - nodes { + nodes { id path diffSide @@ -580,7 +580,7 @@ M.add_pull_request_review_comment_mutation = [[ body pullRequest { reviewThreads(last:100) { - nodes { + nodes { id path diffSide @@ -642,7 +642,7 @@ M.add_pull_request_review_commit_thread_mutation = [[ body pullRequest { reviewThreads(last:100) { - nodes { + nodes { id path diffSide @@ -725,7 +725,7 @@ M.delete_pull_request_review_comment_mutation = [[ pullRequest { id reviewThreads(last:100) { - nodes { + nodes { id path diffSide @@ -1123,8 +1123,8 @@ M.update_pull_request_state_mutation = [[ files(first:100) { nodes { path - viewerViewedState - } + viewerViewedState + } } merged mergedBy { @@ -1246,7 +1246,7 @@ M.update_pull_request_state_mutation = [[ login } } - } + } } ... on MergedEvent { createdAt @@ -1395,7 +1395,7 @@ M.update_pull_request_state_mutation = [[ -- https://docs.github.com/en/graphql/reference/objects#pullrequestreviewthread M.pending_review_threads_query = [[ -query { +query { repository(owner:"%s", name:"%s") { pullRequest (number: %d){ reviews(first:100, states:PENDING) { @@ -1405,7 +1405,7 @@ query { } } reviewThreads(last:100) { - nodes { + nodes { id path diffSide @@ -1539,8 +1539,8 @@ query($endCursor: String) { files(first:100) { nodes { path - viewerViewedState - } + viewerViewedState + } } merged mergedBy { @@ -1599,6 +1599,7 @@ query($endCursor: String) { } } } + %s timelineItems(first: 100, after: $endCursor) { pageInfo { hasNextPage @@ -1655,7 +1656,7 @@ query($endCursor: String) { login } } - } + } } ... on MergedEvent { createdAt @@ -1921,6 +1922,7 @@ query($endCursor: String) { } } } + %s timelineItems(first: 100, after: $endCursor) { pageInfo { hasNextPage @@ -2015,7 +2017,7 @@ query($endCursor: String) { -- https://docs.github.com/en/graphql/reference/unions#issueorpullrequest M.issue_kind_query = [[ -query { +query { repository(owner: "%s", name: "%s") { issueOrPullRequest(number: %d) { __typename @@ -2026,7 +2028,7 @@ query { -- https://docs.github.com/en/graphql/reference/unions#issueorpullrequest M.issue_summary_query = [[ -query { +query { repository(owner: "%s", name: "%s") { issueOrPullRequest(number: %d) { ... on PullRequest { @@ -2125,6 +2127,7 @@ query($endCursor: String) { url repository { nameWithOwner } headRefName + isDraft } pageInfo { hasNextPage @@ -2241,6 +2244,134 @@ M.delete_project_card_mutation = [[ } ]] +-- https://docs.github.com/en/graphql/reference/objects#projectv2 +M.projects_query_v2 = [[ +query { + repository(owner: "%s", name: "%s") { + projects: projectsV2(first: 100) { + nodes { + id + title + url + closed + number + owner { + ... on User { + login + } + ... on Organization { + login + } + } + columns: field(name: "Status") { + ... on ProjectV2SingleSelectField { + id + options { + id + name + } + } + } + } + } + } + user(login: "%s") { + projects: projectsV2(first: 100) { + nodes { + id + title + url + closed + number + owner { + ... on User { + login + } + ... on Organization { + login + } + } + columns: field(name: "Status") { + ... on ProjectV2SingleSelectField { + id + options { + id + name + } + } + } + } + } + } + organization(login: "%s") { + projects: projectsV2(first: 100) { + nodes { + id + title + url + closed + number + owner { + ... on User { + login + } + ... on Organization { + login + } + } + columns: field(name: "Status") { + ... on ProjectV2SingleSelectField { + id + options { + id + name + } + } + } + } + } + } +} +]] + +-- https://docs.github.com/en/graphql/reference/mutations#addprojectv2itembyid +M.add_project_v2_item_mutation = [[ + mutation { + addProjectV2ItemById(input: {contentId: "%s", projectId: "%s"}) { + item { + id + } + } + } +]] + +-- https://docs.github.com/en/graphql/reference/mutations#updateprojectv2itemfieldvalue +M.update_project_v2_item_mutation = [[ + mutation { + updateProjectV2ItemFieldValue( + input: { + projectId: "%s", + itemId: "%s", + fieldId: "%s", + value: { singleSelectOptionId: "%s" } + } + ) { + projectV2Item { + id + } + } + } +]] + +-- https://docs.github.com/en/graphql/reference/mutations#deleteprojectv2item +M.delete_project_v2_item_mutation = [[ + mutation { + deleteProjectV2Item(input: {projectId: "%s", itemId: "%s"}) { + deletedItemId + } + } +]] + -- https://docs.github.com/en/graphql/reference/mutations#createlabel -- requires application/vnd.github.bane-preview+json M.create_label_mutation = [[ @@ -2710,8 +2841,8 @@ M.create_pr_mutation = [[ files(first:100) { nodes { path - viewerViewedState - } + viewerViewedState + } } merged mergedBy { @@ -2822,7 +2953,7 @@ M.create_pr_mutation = [[ login } } - } + } } ... on MergedEvent { createdAt @@ -3040,7 +3171,7 @@ M.create_pr_mutation = [[ -- https://docs.github.com/en/graphql/reference/queries#user M.user_query = [[ -query { +query { user(login:"%s") { id } @@ -3049,7 +3180,7 @@ query { -- https://docs.github.com/en/graphql/reference/objects#pullrequestreviewthread M.repo_labels_query = [[ -query { +query { repository(owner:"%s", name:"%s") { labels(first: 100) { nodes { diff --git a/lua/octo/gh/init.lua b/lua/octo/gh/init.lua index 8027495c..0e585abc 100644 --- a/lua/octo/gh/init.lua +++ b/lua/octo/gh/init.lua @@ -71,6 +71,35 @@ function M.get_user_name(remote_hostname) end end +M.scopes = {} + +function M.setup() + local job = Job:new { + enable_recording = true, + command = "gh", + args = { "auth", "status" }, + env = get_env(), + } + job:sync() + local stdout = table.concat(job:result(), "\n") + local all_scopes = string.match(stdout, "- Token scopes: (.*)") + local split = vim.split(all_scopes, ", ") + + for idx, split_scope in ipairs(split) do + M.scopes[idx] = string.gsub(split_scope, "'", "") + end +end + +function M.has_scope(test_scopes) + for _, test_scope in ipairs(test_scopes) do + if vim.tbl_contains(M.scopes, test_scope) then + return true + end + end + + return false +end + function M.run(opts) if not Job then return diff --git a/lua/octo/init.lua b/lua/octo/init.lua index 1a69add4..1fdecf3f 100644 --- a/lua/octo/init.lua +++ b/lua/octo/init.lua @@ -7,6 +7,7 @@ local completion = require "octo.completion" local folds = require "octo.folds" local gh = require "octo.gh" local graphql = require "octo.gh.graphql" +local fragments = require "octo.gh.fragments" local picker = require "octo.picker" local reviews = require "octo.reviews" local signs = require "octo.ui.signs" @@ -37,6 +38,7 @@ function M.setup(user_config) folds.setup() autocmds.setup() commands.setup() + gh.setup() end function M.configure_octo_buffer(bufnr) @@ -86,11 +88,22 @@ end function M.load(repo, kind, number, cb) local owner, name = utils.split_repo(repo) local query, key + + local pv2_fragment + if gh.has_scope { "read:project", "project" } then + pv2_fragment = fragments.projects_v2_fragment + else + if not config.values.suppress_missing_scope.projects_v2 then + utils.info "Cannot request projects v2, missing scope 'read:project'" + end + pv2_fragment = "" + end + if kind == "pull" then - query = graphql("pull_request_query", owner, name, number) + query = graphql("pull_request_query", owner, name, number, pv2_fragment) key = "pullRequest" elseif kind == "issue" then - query = graphql("issue_query", owner, name, number) + query = graphql("issue_query", owner, name, number, pv2_fragment) key = "issue" elseif kind == "repo" then query = graphql("repository_query", owner, name) diff --git a/lua/octo/navigation.lua b/lua/octo/navigation.lua index af6346d0..69f51949 100644 --- a/lua/octo/navigation.lua +++ b/lua/octo/navigation.lua @@ -51,6 +51,8 @@ function M.open_in_browser(kind, repo, number) cmd = string.format("gh repo view --web -R %s/%s", remote, repo) elseif kind == "gist" then cmd = string.format("gh gist view --web %s", number) + elseif kind == "project" then + cmd = string.format("gh project view --owner %s --web %s", repo, number) end end pcall(vim.cmd, "silent !" .. cmd) diff --git a/lua/octo/pickers/fzf-lua/entry_maker.lua b/lua/octo/pickers/fzf-lua/entry_maker.lua index bb1d15dc..9600abbf 100644 --- a/lua/octo/pickers/fzf-lua/entry_maker.lua +++ b/lua/octo/pickers/fzf-lua/entry_maker.lua @@ -101,6 +101,38 @@ function M.gen_from_project_card(card) } end +function M.gen_from_project_v2(project) + if not project or vim.tbl_isempty(project) then + return nil + end + + local title = project.title + + if project.closed then + title = fzf.utils.ansi_from_hl("Comment", project.title) .. " " .. fzf.utils.ansi_from_hl("OctoPurple", "(closed)") + end + + return { + id = project.id, + repo = project.owner.login, + value = project.number, + ordinal = project.id .. " " .. title, + kind = "project", + obj = project, + } +end + +function M.gen_from_project_v2_column(column) + if not column or vim.tbl_isempty(column) then + return nil + end + return { + value = column.id, + ordinal = column.id .. " " .. column.name, + column = column, + } +end + function M.gen_from_label(label) if not label or vim.tbl_isempty(label) then return nil diff --git a/lua/octo/pickers/fzf-lua/pickers/project_cards.lua b/lua/octo/pickers/fzf-lua/pickers/project_cards.lua index 3a3cbf0a..0b457f52 100644 --- a/lua/octo/pickers/fzf-lua/pickers/project_cards.lua +++ b/lua/octo/pickers/fzf-lua/pickers/project_cards.lua @@ -8,7 +8,7 @@ return function(callback) local buffer = octo_buffers[bufnr] local cards = buffer.node.projectCards if not cards or #cards.nodes == 0 then - utils.error "Cant find any project cards" + utils.error "Can't find any project cards" return end diff --git a/lua/octo/pickers/fzf-lua/pickers/project_cards_v2.lua b/lua/octo/pickers/fzf-lua/pickers/project_cards_v2.lua new file mode 100644 index 00000000..24517da1 --- /dev/null +++ b/lua/octo/pickers/fzf-lua/pickers/project_cards_v2.lua @@ -0,0 +1,53 @@ +local fzf = require "fzf-lua" +local picker_utils = require "octo.pickers.fzf-lua.pickers.utils" +local utils = require "octo.utils" + +return function(callback) + local bufnr = vim.api.nvim_get_current_buf() + local buffer = octo_buffers[bufnr] + local cards = buffer.node.projectItems + if not cards or #cards.nodes == 0 then + utils.error "Can't find any project v2 cards" + return + end + + if #cards.nodes == 1 then + callback(cards.nodes[1].project.id, cards.nodes[1].id) + else + local titles = {} + + for _, card in ipairs(cards.nodes) do + local status = nil + + for _, node in ipairs(card.fieldValues.nodes) do + if node.field ~= nil and node.field.name == "Status" then + status = node.field.name + break + end + end + + if status == nil then + status = "" + end + + table.insert(titles, string.format("%s %s %s (%s)", card.project.id, card.id, status, card.project.title)) + end + + fzf.fzf_exec( + titles, + vim.tbl_deep_extend("force", picker_utils.dropdown_opts, { + fzf_opts = { + ["--no-multi"] = "", -- TODO this can support multi, maybe. + ["--delimiter"] = "' '", + ["--with-nth"] = "3..", + }, + actions = { + ["default"] = function(selected) + local project_id, item_id, _ = unpack(vim.split(selected[1], " ")) + callback(project_id, item_id) + end, + }, + }) + ) + end +end diff --git a/lua/octo/pickers/fzf-lua/pickers/project_columns_v2.lua b/lua/octo/pickers/fzf-lua/pickers/project_columns_v2.lua new file mode 100644 index 00000000..3f0154b9 --- /dev/null +++ b/lua/octo/pickers/fzf-lua/pickers/project_columns_v2.lua @@ -0,0 +1,107 @@ +local entry_maker = require "octo.pickers.fzf-lua.entry_maker" +local fzf = require "fzf-lua" +local gh = require "octo.gh" +local graphql = require "octo.gh.graphql" +local picker_utils = require "octo.pickers.fzf-lua.pickers.utils" +local utils = require "octo.utils" +local fzf_actions = require "octo.pickers.fzf-lua.pickers.fzf_actions" + +return function(cb) + local bufnr = vim.api.nvim_get_current_buf() + local buffer = octo_buffers[bufnr] + if not buffer then + return + end + + local formatted_projects = {} + local common_fzf_opts = vim.tbl_deep_extend("force", picker_utils.dropdown_opts, { + fzf_opts = { + ["--delimiter"] = "' '", + ["--with-nth"] = "2..", + }, + }) + + local get_projects = function(fzf_cb) + local query = graphql("projects_query_v2", buffer.owner, buffer.name, vim.g.octo_viewer, buffer.owner) + gh.run { + args = { "api", "graphql", "--paginate", "-f", string.format("query=%s", query) }, + cb = function(output) + if output then + local resp = vim.fn.json_decode(output) + + local unsorted_projects = {} + local user_projects = resp.data.user and resp.data.user.projects.nodes or {} + local repo_projects = resp.data.repository and resp.data.repository.projects.nodes or {} + local org_projects = not resp.errors and resp.data.organization.projects.nodes or {} + vim.list_extend(unsorted_projects, repo_projects) + vim.list_extend(unsorted_projects, user_projects) + vim.list_extend(unsorted_projects, org_projects) + + local projects = {} + for _, project in ipairs(unsorted_projects) do + if project.closed then + table.insert(projects, #projects + 1, project) + else + table.insert(projects, 0, project) + end + end + + if #projects == 0 then + utils.error(string.format("There are no matching projects for %s.", buffer.repo)) + fzf_cb() + end + + for _, project in ipairs(projects) do + local entry = entry_maker.gen_from_project_v2(project) + + if entry ~= nil then + formatted_projects[entry.ordinal] = entry + fzf_cb(entry.ordinal) + end + end + end + + fzf_cb() + end, + } + end + + local default_action = function(selected_project) + local entry_project = formatted_projects[selected_project[1]] + + local formatted_project_columns = {} + local project_column_titles = {} + + for _, project_column in ipairs(entry_project.obj.columns.options) do + local entry_column = entry_maker.gen_from_project_v2_column(project_column) + + if entry_column ~= nil then + formatted_project_columns[entry_column.ordinal] = entry_column + table.insert(project_column_titles, entry_column.ordinal) + end + end + + fzf.fzf_exec( + project_column_titles, + vim.tbl_deep_extend("force", common_fzf_opts, { + actions = { + ["default"] = function(selected_column) + local entry_column = formatted_project_columns[selected_column[1]] + cb(entry_project.id, entry_project.obj.columns.id, entry_column.value) + end, + }, + }) + ) + end + + fzf.fzf_exec( + get_projects, + vim.tbl_deep_extend("force", common_fzf_opts, { + actions = vim.tbl_deep_extend("force", fzf_actions.common_open_actions(formatted_projects), { + ["default"] = { + default_action, + }, + }), + }) + ) +end diff --git a/lua/octo/pickers/fzf-lua/pickers/prs.lua b/lua/octo/pickers/fzf-lua/pickers/prs.lua index 380455b7..90ae3e23 100644 --- a/lua/octo/pickers/fzf-lua/pickers/prs.lua +++ b/lua/octo/pickers/fzf-lua/pickers/prs.lua @@ -59,7 +59,13 @@ return function(opts) if entry ~= nil then formatted_pulls[entry.ordinal] = entry - local prefix = fzf.utils.ansi_from_hl("Comment", entry.value) + local highlight + if entry.obj.isDraft then + highlight = "OctoSymbol" + else + highlight = "OctoStateOpen" + end + local prefix = fzf.utils.ansi_from_hl(highlight, entry.value) fzf_cb(prefix .. " " .. entry.obj.title) end end diff --git a/lua/octo/pickers/fzf-lua/pickers/search.lua b/lua/octo/pickers/fzf-lua/pickers/search.lua index 9780af67..5ebb12d7 100644 --- a/lua/octo/pickers/fzf-lua/pickers/search.lua +++ b/lua/octo/pickers/fzf-lua/pickers/search.lua @@ -71,6 +71,7 @@ return function(opts) prompt = picker_utils.get_prompt(opts.prompt_title), func_async_callback = false, previewer = previewers.search(), + query_delay = 500, fzf_opts = { ["--info"] = "default", ["--delimiter"] = "' '", diff --git a/lua/octo/pickers/fzf-lua/previewers.lua b/lua/octo/pickers/fzf-lua/previewers.lua index 062d7a42..47f20927 100644 --- a/lua/octo/pickers/fzf-lua/previewers.lua +++ b/lua/octo/pickers/fzf-lua/previewers.lua @@ -5,7 +5,6 @@ local graphql = require "octo.gh.graphql" local utils = require "octo.utils" local writers = require "octo.ui.writers" local config = require "octo.config" -local log = require "octo.pickers.fzf-lua.log" local M = {} diff --git a/lua/octo/pickers/fzf-lua/provider.lua b/lua/octo/pickers/fzf-lua/provider.lua index 4fde1b81..efb8fc8b 100644 --- a/lua/octo/pickers/fzf-lua/provider.lua +++ b/lua/octo/pickers/fzf-lua/provider.lua @@ -12,7 +12,9 @@ M.picker = { labels = require "octo.pickers.fzf-lua.pickers.labels", pending_threads = require "octo.pickers.fzf-lua.pickers.pending_threads", project_cards = require "octo.pickers.fzf-lua.pickers.project_cards", + project_cards_v2 = require "octo.pickers.fzf-lua.pickers.project_cards_v2", project_columns = require "octo.pickers.fzf-lua.pickers.project_columns", + project_columns_v2 = require "octo.pickers.fzf-lua.pickers.project_columns_v2", prs = require "octo.pickers.fzf-lua.pickers.prs", repos = require "octo.pickers.fzf-lua.pickers.repos", review_commits = require "octo.pickers.fzf-lua.pickers.review_commits", diff --git a/lua/octo/pickers/telescope/provider.lua b/lua/octo/pickers/telescope/provider.lua index a4afc37f..6aefdaa5 100644 --- a/lua/octo/pickers/telescope/provider.lua +++ b/lua/octo/pickers/telescope/provider.lua @@ -17,6 +17,10 @@ local sorters = require "telescope.sorters" local M = {} +function M.not_implemented() + utils.error "Not implemented yet" +end + local dropdown_opts = require("telescope.themes").get_dropdown { layout_config = { width = 0.4, @@ -1048,7 +1052,9 @@ M.picker = { changed_files = M.changed_files, pending_threads = M.pending_threads, project_cards = M.select_project_card, + project_cards_v2 = M.not_implemented, project_columns = M.select_target_project_column, + project_columns_v2 = M.not_implemented, labels = M.select_label, assigned_labels = M.select_assigned_label, users = M.select_user, diff --git a/lua/octo/pickers/vim-clap/provider.lua b/lua/octo/pickers/vim-clap/provider.lua index b59dbc8f..f5f949ac 100644 --- a/lua/octo/pickers/vim-clap/provider.lua +++ b/lua/octo/pickers/vim-clap/provider.lua @@ -15,7 +15,9 @@ M.picker = { changed_files = M.not_implemented, pending_threads = M.not_implemented, project_cards = M.not_implemented, + project_cards_v2 = M.not_implemented, project_columns = M.not_implemented, + project_columns_v2 = M.not_implemented, labels = M.not_implemented, assigned_labels = M.not_implemented, users = M.not_implemented, diff --git a/lua/octo/ui/writers.lua b/lua/octo/ui/writers.lua index 8c9f87e8..61b6bb7f 100644 --- a/lua/octo/ui/writers.lua +++ b/lua/octo/ui/writers.lua @@ -305,6 +305,41 @@ function M.write_details(bufnr, issue, update) table.insert(details, projects_vt) end + -- projects v2 + if issue.projectItems and #issue.projectItems.nodes > 0 then + local projects_vt = { + { "Projects (v2): ", "OctoDetailsLabel" }, + } + --local project_color = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID("NormalFloat")), "bg#"):sub(2) + --local column_color = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID("Comment")), "fg#"):sub(2) + for idx, item in ipairs(issue.projectItems.nodes) do + if item.project ~= vim.NIL then + if idx >= 2 then + table.insert(projects_vt, { ", " }) + end + + local status = nil + + for _, fieldValues in ipairs(item.fieldValues.nodes) do + if fieldValues.field ~= nil and fieldValues.field.name == "Status" then + status = fieldValues.name + end + end + + if status == nil then + table.insert(projects_vt, { "No status", "OctoRed" }) + else + table.insert(projects_vt, { status }) + end + + table.insert(projects_vt, { " (", "OctoDetailsLabel" }) + table.insert(projects_vt, { item.project.title }) + table.insert(projects_vt, { ")", "OctoDetailsLabel" }) + end + end + table.insert(details, projects_vt) + end + -- milestones local ms = issue.milestone local milestone_vt = { diff --git a/lua/tests/octo/config_spec.lua b/lua/tests/octo/config_spec.lua index b29f5d9b..bad224b0 100644 --- a/lua/tests/octo/config_spec.lua +++ b/lua/tests/octo/config_spec.lua @@ -106,6 +106,16 @@ describe("Octo config", function() assert.True(vim.tbl_count(require("octo.config").validate_config()) ~= 0) end) + it("should return invalid when default_to_projects_v2 isn't a boolean", function() + config.values.default_to_projects_v2 = "not a boolean" + assert.True(vim.tbl_count(require("octo.config").validate_config()) ~= 0) + end) + + it("should return invalid when suppress_missing_scope isn't a table", function() + config.values.suppress_missing_scope = "not a table" + assert.True(vim.tbl_count(require("octo.config").validate_config()) ~= 0) + end) + it("should return invalid when ui isn't a table", function() config.values.ui = "not a table" assert.True(vim.tbl_count(require("octo.config").validate_config()) ~= 0)