Adding Support for Resolving/Unresolving Discussions (#39)
This MR adds the ability to mark discussions as resolved or unresolved. This is important to the review process.
This commit is contained in:
committed by
GitHub
parent
1676992266
commit
d25c62ae9f
25
README.md
25
README.md
@@ -4,8 +4,11 @@ This Neovim plugin is designed to make it easy to review Gitlab MRs from within
|
|||||||
|
|
||||||
- Create, edit, delete, and reply to comments on an MR
|
- Create, edit, delete, and reply to comments on an MR
|
||||||
- Read and Edit an MR description
|
- Read and Edit an MR description
|
||||||
- Approve/Revoke Approval for an MR
|
- Approve or revoke ppproval for an MR
|
||||||
- Add or remove reviewers and assignees
|
- Add or remove reviewers and assignees
|
||||||
|
- Resolve and unresolve discussion threads
|
||||||
|
|
||||||
|
And a lot more!
|
||||||
|
|
||||||
https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dfd3aa8a-6fc4-4e43-8d2f-489df0745822
|
https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dfd3aa8a-6fc4-4e43-8d2f-489df0745822
|
||||||
|
|
||||||
@@ -54,7 +57,7 @@ use {
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
This plugin requires a `.gitlab.nvim` file in the root of the local Gitlab directory. Provide this file with values required to connect to your gitlab instance (gitlab_url is optional, use only for self-hosted instances):
|
This plugin requires a `.gitlab.nvim` file in the root of the local Gitlab directory. Provide this file with values required to connect to your gitlab instance (gitlab_url is optional, use ONLY for self-hosted instances):
|
||||||
|
|
||||||
```
|
```
|
||||||
project_id=112415
|
project_id=112415
|
||||||
@@ -92,14 +95,15 @@ require("gitlab").setup({
|
|||||||
perform_action = "<leader>s", -- Once in normal mode, does action (like saving comment or editing description, etc)
|
perform_action = "<leader>s", -- Once in normal mode, does action (like saving comment or editing description, etc)
|
||||||
},
|
},
|
||||||
discussion_tree = { -- The discussion tree that holds all comments
|
discussion_tree = { -- The discussion tree that holds all comments
|
||||||
jump_to_location = "o",
|
jump_to_location = "o", -- Jump to comment location in file
|
||||||
edit_comment = "e",
|
edit_comment = "e", -- Edit coment
|
||||||
delete_comment = "dd",
|
delete_comment = "dd", -- Delete comment
|
||||||
reply_to_comment = "r",
|
reply_to_comment = "r", -- Reply to comment
|
||||||
toggle_node = "t",
|
toggle_resolved = "p" -- Toggles the resolved status of the discussion
|
||||||
|
toggle_node = "t", -- Opens or closes the discussion
|
||||||
position = "left", -- "top", "right", "bottom" or "left"
|
position = "left", -- "top", "right", "bottom" or "left"
|
||||||
|
relative = "editor" -- Position of tree split relative to "editor" or "window"
|
||||||
size = "20%", -- Size of split
|
size = "20%", -- Size of split
|
||||||
relative = "editor" -- Position relative to "editor" or "window"
|
|
||||||
},
|
},
|
||||||
dialogue = { -- The confirmation dialogue for deleting comments
|
dialogue = { -- The confirmation dialogue for deleting comments
|
||||||
focus_next = { "j", "<Down>", "<Tab>" },
|
focus_next = { "j", "<Down>", "<Tab>" },
|
||||||
@@ -107,6 +111,10 @@ require("gitlab").setup({
|
|||||||
close = { "<Esc>", "<C-c>" },
|
close = { "<Esc>", "<C-c>" },
|
||||||
submit = { "<CR>", "<Space>" },
|
submit = { "<CR>", "<Space>" },
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
resolved = '✓', -- Symbol to show next to resolved discussions
|
||||||
|
unresolved = '✖', -- Symbol to show next to unresolved discussions
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@@ -179,6 +187,7 @@ Within the discussion tree, there are several functions that you can call, howev
|
|||||||
require("gitlab").delete_comment()
|
require("gitlab").delete_comment()
|
||||||
require("gitlab").edit_comment()
|
require("gitlab").edit_comment()
|
||||||
require("gitlab").reply()
|
require("gitlab").reply()
|
||||||
|
require("gitlab").toggle_resolved()
|
||||||
```
|
```
|
||||||
|
|
||||||
## Keybindings
|
## Keybindings
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type EditCommentRequest struct {
|
|||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
NoteId int `json:"note_id"`
|
NoteId int `json:"note_id"`
|
||||||
DiscussionId string `json:"discussion_id"`
|
DiscussionId string `json:"discussion_id"`
|
||||||
|
Resolved bool `json:"resolved"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommentResponse struct {
|
type CommentResponse struct {
|
||||||
@@ -158,26 +159,34 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
options := gitlab.UpdateMergeRequestDiscussionNoteOptions{
|
options := gitlab.UpdateMergeRequestDiscussionNoteOptions{}
|
||||||
Body: gitlab.String(editCommentRequest.Comment),
|
|
||||||
|
/* The PATCH can either be to the resolved status of
|
||||||
|
the discussion or or the text of the comment */
|
||||||
|
msg := "edit comment"
|
||||||
|
if editCommentRequest.Comment == "" {
|
||||||
|
options.Resolved = &editCommentRequest.Resolved
|
||||||
|
msg = "update discussion status"
|
||||||
|
} else {
|
||||||
|
options.Body = gitlab.String(editCommentRequest.Comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
note, res, err := c.git.Discussions.UpdateMergeRequestDiscussionNote(c.projectId, c.mergeId, editCommentRequest.DiscussionId, editCommentRequest.NoteId, &options)
|
note, res, err := c.git.Discussions.UpdateMergeRequestDiscussionNote(c.projectId, c.mergeId, editCommentRequest.DiscussionId, editCommentRequest.NoteId, &options)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not edit comment", res.StatusCode)
|
c.handleError(w, err, "Could not "+msg, res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(res.StatusCode)
|
w.WriteHeader(res.StatusCode)
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
c.handleError(w, errors.New("Non-200 status code recieved"), "Could not edit comment", res.StatusCode)
|
c.handleError(w, errors.New("Non-200 status code recieved"), "Could not "+msg, res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
response := CommentResponse{
|
response := CommentResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Message: "Comment edited succesfully",
|
Message: "Comment updated succesfully",
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
},
|
},
|
||||||
Comment: note,
|
Comment: note,
|
||||||
|
|||||||
@@ -143,11 +143,28 @@ M.send_edits = function(discussion_id, note_id)
|
|||||||
local json = vim.json.encode(json_table)
|
local json = vim.json.encode(json_table)
|
||||||
job.run_job("comment", "PATCH", json, function(data)
|
job.run_job("comment", "PATCH", json, function(data)
|
||||||
vim.notify(data.message, vim.log.levels.INFO)
|
vim.notify(data.message, vim.log.levels.INFO)
|
||||||
M.redraw_node(text)
|
M.redraw_text(text)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M.toggle_resolved = function()
|
||||||
|
local note = state.tree:get_node()
|
||||||
|
if not note.resolvable then return end
|
||||||
|
|
||||||
|
local json_table = {
|
||||||
|
discussion_id = note.id,
|
||||||
|
note_id = note.root_note_id,
|
||||||
|
resolved = not note.resolved,
|
||||||
|
}
|
||||||
|
|
||||||
|
local json = vim.json.encode(json_table)
|
||||||
|
job.run_job("comment", "PATCH", json, function(data)
|
||||||
|
vim.notify(data.message, vim.log.levels.INFO)
|
||||||
|
M.update_resolved_status(note, not note.resolved)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
-- Helpers
|
-- Helpers
|
||||||
M.find_deletion_commit = function(file)
|
M.find_deletion_commit = function(file)
|
||||||
local current_line = vim.api.nvim_get_current_line()
|
local current_line = vim.api.nvim_get_current_line()
|
||||||
@@ -167,7 +184,35 @@ M.find_deletion_commit = function(file)
|
|||||||
return words[2]
|
return words[2]
|
||||||
end
|
end
|
||||||
|
|
||||||
M.redraw_node = function(text)
|
M.update_resolved_status = function(note, mark_resolved)
|
||||||
|
local current_text = state.tree.nodes.by_id["-" .. note.id].text
|
||||||
|
local target = mark_resolved and 'resolved' or 'unresolved'
|
||||||
|
local current = mark_resolved and 'unresolved' or 'resolved'
|
||||||
|
|
||||||
|
local function set_property(key, val)
|
||||||
|
state.tree.nodes.by_id["-" .. note.id][key] = val
|
||||||
|
end
|
||||||
|
|
||||||
|
local has_symbol = function(s)
|
||||||
|
return state.SYMBOLS[s] ~= nil and state.SYMBOLS[s] ~= ''
|
||||||
|
end
|
||||||
|
|
||||||
|
set_property('resolved', mark_resolved)
|
||||||
|
|
||||||
|
if not has_symbol(current) and not has_symbol(target) then return end
|
||||||
|
|
||||||
|
if not has_symbol(current) and has_symbol(target) then
|
||||||
|
set_property('text', (current_text .. " " .. state.SYMBOLS[target]))
|
||||||
|
elseif has_symbol(current) and not has_symbol(target) then
|
||||||
|
set_property('text', u.remove_last_chunk(current_text))
|
||||||
|
else
|
||||||
|
set_property('text', (u.remove_last_chunk(current_text) .. " " .. state.SYMBOLS[target]))
|
||||||
|
end
|
||||||
|
|
||||||
|
state.tree:render()
|
||||||
|
end
|
||||||
|
|
||||||
|
M.redraw_text = function(text)
|
||||||
local current_node = state.tree:get_node()
|
local current_node = state.tree:get_node()
|
||||||
local note_node = discussions.get_note_node(current_node)
|
local note_node = discussions.get_note_node(current_node)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ M.list_discussions = function()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local splitState = state.DISCUSSION_SPLIT
|
local splitState = state.DISCUSSION.SPLIT
|
||||||
splitState.buf_options = { modifiable = false }
|
splitState.buf_options = { modifiable = false }
|
||||||
local split = NuiSplit(splitState)
|
local split = NuiSplit(splitState)
|
||||||
split:mount()
|
split:mount()
|
||||||
@@ -84,6 +84,10 @@ M.set_tree_keymaps = function(buf)
|
|||||||
require("gitlab.comment").delete_comment()
|
require("gitlab.comment").delete_comment()
|
||||||
end, { buffer = true })
|
end, { buffer = true })
|
||||||
|
|
||||||
|
vim.keymap.set('n', state.keymaps.discussion_tree.toggle_resolved, function()
|
||||||
|
require("gitlab.comment").toggle_resolved()
|
||||||
|
end, { buffer = true })
|
||||||
|
|
||||||
-- Expand/collapse the current node
|
-- Expand/collapse the current node
|
||||||
vim.keymap.set('n', state.keymaps.discussion_tree.toggle_node, function()
|
vim.keymap.set('n', state.keymaps.discussion_tree.toggle_node, function()
|
||||||
local node = state.tree:get_node()
|
local node = state.tree:get_node()
|
||||||
@@ -134,7 +138,7 @@ M.get_note_node = function(node)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
M.build_note_body = function(note)
|
M.build_note_body = function(note, resolve_info)
|
||||||
local text_nodes = {}
|
local text_nodes = {}
|
||||||
for bodyLine in note.body:gmatch("[^\n]+") do
|
for bodyLine in note.body:gmatch("[^\n]+") do
|
||||||
local line = u.attach_uuid(bodyLine)
|
local line = u.attach_uuid(bodyLine)
|
||||||
@@ -145,23 +149,26 @@ M.build_note_body = function(note)
|
|||||||
}, {}))
|
}, {}))
|
||||||
end
|
end
|
||||||
|
|
||||||
local noteHeader = "@" ..
|
local resolve_symbol = ''
|
||||||
note.author.username .. " " .. u.format_date(note.created_at)
|
if resolve_info ~= nil and resolve_info.resolvable then
|
||||||
|
resolve_symbol = resolve_info.resolved and state.SYMBOLS.resolved or state.SYMBOLS.unresolved
|
||||||
|
end
|
||||||
|
|
||||||
|
local noteHeader = "@" .. note.author.username .. " " .. u.format_date(note.created_at) .. " " .. resolve_symbol
|
||||||
|
|
||||||
return noteHeader, text_nodes
|
return noteHeader, text_nodes
|
||||||
end
|
end
|
||||||
|
|
||||||
M.build_note = function(note)
|
M.build_note = function(note, resolve_info)
|
||||||
local text, text_nodes = M.build_note_body(note)
|
local text, text_nodes = M.build_note_body(note, resolve_info)
|
||||||
local line_number = note.position.new_line or note.position.old_line
|
local line_number = note.position.new_line or note.position.old_line
|
||||||
local note_node = NuiTree.Node(
|
local note_node = NuiTree.Node({
|
||||||
{
|
text = text,
|
||||||
text = text,
|
id = note.id,
|
||||||
id = note.id,
|
file_name = note.position.new_path,
|
||||||
file_name = note.position.new_path,
|
line_number = line_number,
|
||||||
line_number = line_number,
|
is_note = true,
|
||||||
is_note = true
|
}, text_nodes)
|
||||||
}, text_nodes)
|
|
||||||
|
|
||||||
return note_node, text, text_nodes
|
return note_node, text, text_nodes
|
||||||
end
|
end
|
||||||
@@ -212,14 +219,18 @@ M.add_discussions_to_table = function(discussions)
|
|||||||
local root_file_name = ''
|
local root_file_name = ''
|
||||||
local root_id = 0
|
local root_id = 0
|
||||||
local root_text_nodes = {}
|
local root_text_nodes = {}
|
||||||
|
local resolvable = false
|
||||||
|
local resolved = false
|
||||||
|
|
||||||
for j, note in ipairs(discussion.notes) do
|
for j, note in ipairs(discussion.notes) do
|
||||||
if j == 1 then
|
if j == 1 then
|
||||||
__, root_text, root_text_nodes = M.build_note(note)
|
__, root_text, root_text_nodes = M.build_note(note, { resolved = note.resolved, resolvable = note.resolvable })
|
||||||
root_file_name = note.position.new_path
|
root_file_name = note.position.new_path
|
||||||
root_line_number = note.position.new_line or note.position.old_line
|
root_line_number = note.position.new_line or note.position.old_line
|
||||||
root_id = discussion.id
|
root_id = discussion.id
|
||||||
root_note_id = note.id
|
root_note_id = note.id
|
||||||
|
resolvable = note.resolvable
|
||||||
|
resolved = note.resolved
|
||||||
else -- Otherwise insert it as a child node...
|
else -- Otherwise insert it as a child node...
|
||||||
local note_node = M.build_note(note)
|
local note_node = M.build_note(note)
|
||||||
table.insert(discussion_children, note_node)
|
table.insert(discussion_children, note_node)
|
||||||
@@ -236,6 +247,8 @@ M.add_discussions_to_table = function(discussions)
|
|||||||
root_note_id = root_note_id,
|
root_note_id = root_note_id,
|
||||||
file_name = root_file_name,
|
file_name = root_file_name,
|
||||||
line_number = root_line_number,
|
line_number = root_line_number,
|
||||||
|
resolvable = resolvable,
|
||||||
|
resolved = resolved
|
||||||
}, body)
|
}, body)
|
||||||
|
|
||||||
table.insert(t, root_node)
|
table.insert(t, root_node)
|
||||||
|
|||||||
@@ -43,10 +43,11 @@ local M = {}
|
|||||||
M.summary = ensureState(summary.summary)
|
M.summary = ensureState(summary.summary)
|
||||||
M.approve = ensureState(job.approve)
|
M.approve = ensureState(job.approve)
|
||||||
M.revoke = ensureState(job.revoke)
|
M.revoke = ensureState(job.revoke)
|
||||||
M.create_comment = ensureState(comment.create_comment)
|
|
||||||
M.list_discussions = ensureState(discussions.list_discussions)
|
M.list_discussions = ensureState(discussions.list_discussions)
|
||||||
|
M.create_comment = ensureState(comment.create_comment)
|
||||||
M.edit_comment = ensureState(comment.edit_comment)
|
M.edit_comment = ensureState(comment.edit_comment)
|
||||||
M.delete_comment = ensureState(comment.delete_comment)
|
M.delete_comment = ensureState(comment.delete_comment)
|
||||||
|
M.toggle_resolved = ensureState(comment.toggle_resolved)
|
||||||
M.add_reviewer = ensureProjectMembers(ensureState(assignees_and_reviewers.add_reviewer))
|
M.add_reviewer = ensureProjectMembers(ensureState(assignees_and_reviewers.add_reviewer))
|
||||||
M.delete_reviewer = ensureProjectMembers(ensureState(assignees_and_reviewers.delete_reviewer))
|
M.delete_reviewer = ensureProjectMembers(ensureState(assignees_and_reviewers.delete_reviewer))
|
||||||
M.add_assignee = ensureProjectMembers(ensureState(assignees_and_reviewers.add_assignee))
|
M.add_assignee = ensureProjectMembers(ensureState(assignees_and_reviewers.add_assignee))
|
||||||
@@ -160,10 +161,17 @@ M.setPluginConfiguration = function(args)
|
|||||||
-- Configuration for the plugin, such as port of server, layout, etc
|
-- Configuration for the plugin, such as port of server, layout, etc
|
||||||
state.PORT = args.port or 21036
|
state.PORT = args.port or 21036
|
||||||
state.LOG_PATH = args.log_path or (vim.fn.stdpath("cache") .. "/gitlab.nvim.log")
|
state.LOG_PATH = args.log_path or (vim.fn.stdpath("cache") .. "/gitlab.nvim.log")
|
||||||
state.DISCUSSION_SPLIT = {
|
state.DISCUSSION = {
|
||||||
relative = args.keymaps and args.keymaps.discussion_tree and args.keymaps.discussion_tree.relative or "editor",
|
SPLIT = {
|
||||||
position = args.keymaps and args.keymaps.discussion_tree and args.keymaps.discussion_tree.position or "left",
|
relative = args.keymaps and args.keymaps.discussion_tree and args.keymaps.discussion_tree.relative or "editor",
|
||||||
size = args.keymaps and args.keymaps.discussion_tree and args.keymaps.discussion_tree.size or "20%",
|
position = args.keymaps and args.keymaps.discussion_tree and args.keymaps.discussion_tree.position or "left",
|
||||||
|
size = args.keymaps and args.keymaps.discussion_tree and args.keymaps.discussion_tree.size or "20%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SYMBOLS = {
|
||||||
|
resolved = (args.symbols and args.symbols.resolved or '✓'),
|
||||||
|
unresolved = (args.symbols and args.symbols.unresolved or '')
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ M.keymaps = {
|
|||||||
delete_comment = "dd",
|
delete_comment = "dd",
|
||||||
reply_to_comment = "r",
|
reply_to_comment = "r",
|
||||||
toggle_node = "t",
|
toggle_node = "t",
|
||||||
|
toggle_resolved = "p"
|
||||||
},
|
},
|
||||||
dialogue = {
|
dialogue = {
|
||||||
focus_next = { "j", "<Down>", "<Tab>" },
|
focus_next = { "j", "<Down>", "<Tab>" },
|
||||||
|
|||||||
@@ -284,7 +284,17 @@ local extract = function(t, property)
|
|||||||
return resultTable
|
return resultTable
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local remove_last_chunk = function(sentence)
|
||||||
|
local words = {}
|
||||||
|
for word in sentence:gmatch("%S+") do
|
||||||
|
table.insert(words, word)
|
||||||
|
end
|
||||||
|
table.remove(words, #words)
|
||||||
|
local sentence_without_last = table.concat(words, " ")
|
||||||
|
return sentence_without_last
|
||||||
|
end
|
||||||
|
|
||||||
|
M.remove_last_chunk = remove_last_chunk
|
||||||
M.extract = extract
|
M.extract = extract
|
||||||
M.contains = contains
|
M.contains = contains
|
||||||
M.attach_uuid = attach_uuid
|
M.attach_uuid = attach_uuid
|
||||||
|
|||||||
Reference in New Issue
Block a user