diff --git a/README.md b/README.md
index fda1751..0b81127 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,9 @@ return {
"harrisoncramer/gitlab.nvim",
dependencies = {
"MunifTanjim/nui.nvim",
- "nvim-lua/plenary.nvim"
+ "nvim-lua/plenary.nvim",
+ "stevearc/dressing.nvim" -- Recommended but not required. Better UI for pickers.
+ enabled = true,
},
build = function () require("gitlab").build() end, -- Builds the Go binary
config = function()
@@ -143,6 +145,24 @@ The `comment` command will open up a NUI popover that will allow you to create a
require("gitlab").create_comment()
```
+The `add_reviewer` and `delete_reviewer` commands, as well as the `add_assignee` and `delete_assignee` functions, will let you choose from a list of users who are availble in the current project:
+
+```lua
+require("gitlab").add_reviewer()
+require("gitlab").delete_reviewer()
+require("gitlab").add_assignee()
+require("gitlab").delete_assignee()
+```
+
+These commands use Neovim's built in picker, which is much nicer if you install dressing. If you use Dressing, please enable it:
+
+```lua
+require("dressing").setup({
+ input = {
+ enabled = true
+ }
+})
+
### Discussions
Gitlab groups threads of notes together into "disucssions." To get a list of all the discussions for the current MR, use the `list_discussions` command. This command will open up a split view of all the comments on the current merge request. You can jump to the comment location by using the `o` key in the tree buffer, and you can reply to a thread by using the `r` keybinding in the tree buffer:
@@ -170,6 +190,10 @@ vim.keymap.set("n", "glA", gitlab.approve)
vim.keymap.set("n", "glR", gitlab.revoke)
vim.keymap.set("n", "glc", gitlab.create_comment)
vim.keymap.set("n", "gld", gitlab.list_discussions)
+vim.keymap.set("n", "glaa", gitlab.add_assignee)
+vim.keymap.set("n", "glad", gitlab.delete_assignee)
+vim.keymap.set("n", "glra", gitlab.add_reviewer)
+vim.keymap.set("n", "glrd", gitlab.delete_reviewer)
```
## Troubleshooting
diff --git a/cmd/assignee.go b/cmd/assignee.go
new file mode 100644
index 0000000..646f6f2
--- /dev/null
+++ b/cmd/assignee.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+
+ "github.com/xanzy/go-gitlab"
+)
+
+type AssigneeUpdateRequest struct {
+ Ids []int `json:"ids"`
+}
+
+type AssigneeUpdateResponse struct {
+ SuccessResponse
+ Assignees []*gitlab.BasicUser `json:"assignees"`
+}
+
+type AssigneesRequestResponse struct {
+ SuccessResponse
+ Assignees []int `json:"assignees"`
+}
+
+func AssigneesHandler(w http.ResponseWriter, r *http.Request) {
+ c := r.Context().Value("client").(Client)
+ w.Header().Set("Content-Type", "application/json")
+
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
+ return
+ }
+
+ defer r.Body.Close()
+ var assigneeUpdateRequest AssigneeUpdateRequest
+ err = json.Unmarshal(body, &assigneeUpdateRequest)
+
+ if err != nil {
+ c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
+ return
+ }
+
+ mr, res, err := c.git.MergeRequests.UpdateMergeRequest(c.projectId, c.mergeId, &gitlab.UpdateMergeRequestOptions{
+ AssigneeIDs: &assigneeUpdateRequest.Ids,
+ })
+
+ if err != nil {
+ c.handleError(w, err, "Could not modify merge request assignees", http.StatusBadRequest)
+ return
+ }
+
+ if res.StatusCode != http.StatusOK {
+ c.handleError(w, err, "Could not modify merge request assignees", http.StatusBadRequest)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ response := AssigneeUpdateResponse{
+ SuccessResponse: SuccessResponse{
+ Message: "Assignees updated",
+ Status: http.StatusOK,
+ },
+ Assignees: mr.Assignees,
+ }
+
+ json.NewEncoder(w).Encode(response)
+
+}
diff --git a/cmd/update.go b/cmd/description.go
similarity index 67%
rename from cmd/update.go
rename to cmd/description.go
index 9dfc30e..3ebcbe9 100644
--- a/cmd/update.go
+++ b/cmd/description.go
@@ -9,16 +9,16 @@ import (
"github.com/xanzy/go-gitlab"
)
-type UpdateRequest struct {
+type DescriptionUpdateRequest struct {
Description string `json:"description"`
}
-type UpdateResponse struct {
+type DescriptionUpdateResponse struct {
SuccessResponse
MergeRequest *gitlab.MergeRequest `json:"mr"`
}
-func UpdateHandler(w http.ResponseWriter, r *http.Request) {
+func DescriptionHandler(w http.ResponseWriter, r *http.Request) {
c := r.Context().Value("client").(Client)
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPut {
@@ -33,31 +33,31 @@ func UpdateHandler(w http.ResponseWriter, r *http.Request) {
}
defer r.Body.Close()
- var updateRequest UpdateRequest
- err = json.Unmarshal(body, &updateRequest)
+ var DescriptionUpdateRequest DescriptionUpdateRequest
+ err = json.Unmarshal(body, &DescriptionUpdateRequest)
if err != nil {
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
return
}
- mr, res, err := c.git.MergeRequests.UpdateMergeRequest(c.projectId, c.mergeId, &gitlab.UpdateMergeRequestOptions{Description: &updateRequest.Description})
+ mr, res, err := c.git.MergeRequests.UpdateMergeRequest(c.projectId, c.mergeId, &gitlab.UpdateMergeRequestOptions{Description: &DescriptionUpdateRequest.Description})
if err != nil {
- c.handleError(w, err, "Could not edit merge request", http.StatusBadRequest)
+ c.handleError(w, err, "Could not edit merge request description", http.StatusBadRequest)
return
}
if res.StatusCode != http.StatusOK {
- c.handleError(w, err, "Could not edit merge request", http.StatusBadRequest)
+ c.handleError(w, err, "Could not edit merge request description", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
- response := UpdateResponse{
+ response := DescriptionUpdateResponse{
SuccessResponse: SuccessResponse{
- Message: "Merge request updated",
+ Message: "Description updated",
Status: http.StatusOK,
},
MergeRequest: mr,
diff --git a/cmd/main.go b/cmd/main.go
index cf25c1a..e81cc44 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -29,13 +29,16 @@ func main() {
}
m := http.NewServeMux()
- m.Handle("/mr", withGitlabContext(http.HandlerFunc(UpdateHandler), c))
+ m.Handle("/mr/description", withGitlabContext(http.HandlerFunc(DescriptionHandler), c))
+ m.Handle("/mr/reviewer", withGitlabContext(http.HandlerFunc(ReviewersHandler), c))
+ m.Handle("/mr/assignee", withGitlabContext(http.HandlerFunc(AssigneesHandler), c))
m.Handle("/approve", withGitlabContext(http.HandlerFunc(ApproveHandler), c))
m.Handle("/revoke", withGitlabContext(http.HandlerFunc(RevokeHandler), c))
m.Handle("/info", withGitlabContext(http.HandlerFunc(InfoHandler), c))
m.Handle("/discussions", withGitlabContext(http.HandlerFunc(ListDiscussionsHandler), c))
m.Handle("/comment", withGitlabContext(http.HandlerFunc(CommentHandler), c))
m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c))
+ m.Handle("/members", withGitlabContext(http.HandlerFunc(ProjectMembersHandler), c))
port := fmt.Sprintf(":%s", os.Args[3])
server := &http.Server{
diff --git a/cmd/members.go b/cmd/members.go
new file mode 100644
index 0000000..c5c3634
--- /dev/null
+++ b/cmd/members.go
@@ -0,0 +1,43 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/xanzy/go-gitlab"
+)
+
+type ProjectMembersResponse struct {
+ SuccessResponse
+ ProjectMembers []*gitlab.ProjectMember
+}
+
+func ProjectMembersHandler(w http.ResponseWriter, r *http.Request) {
+ c := r.Context().Value("client").(Client)
+ w.Header().Set("Content-Type", "application/json")
+
+ projectMemberOptions := gitlab.ListProjectMembersOptions{
+ ListOptions: gitlab.ListOptions{
+ PerPage: 100,
+ },
+ }
+
+ projectMembers, res, err := c.git.ProjectMembers.ListAllProjectMembers(c.projectId, &projectMemberOptions)
+ if err != nil {
+ c.handleError(w, err, "Could not fetch project users", res.StatusCode)
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ response := ProjectMembersResponse{
+ SuccessResponse: SuccessResponse{
+ Status: http.StatusOK,
+ Message: "Project users fetched successfully",
+ },
+ ProjectMembers: projectMembers,
+ }
+
+ json.NewEncoder(w).Encode(response)
+
+ return
+}
diff --git a/cmd/reviewer.go b/cmd/reviewer.go
new file mode 100644
index 0000000..39d9b62
--- /dev/null
+++ b/cmd/reviewer.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+
+ "github.com/xanzy/go-gitlab"
+)
+
+type ReviewerUpdateRequest struct {
+ Ids []int `json:"ids"`
+}
+
+type ReviewerUpdateResponse struct {
+ SuccessResponse
+ Reviewers []*gitlab.BasicUser `json:"reviewers"`
+}
+
+type ReviewersRequestResponse struct {
+ SuccessResponse
+ Reviewers []int `json:"reviewers"`
+}
+
+func ReviewersHandler(w http.ResponseWriter, r *http.Request) {
+ c := r.Context().Value("client").(Client)
+ w.Header().Set("Content-Type", "application/json")
+
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
+ return
+ }
+
+ defer r.Body.Close()
+ var reviewerUpdateRequest ReviewerUpdateRequest
+ err = json.Unmarshal(body, &reviewerUpdateRequest)
+
+ if err != nil {
+ c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
+ return
+ }
+
+ mr, res, err := c.git.MergeRequests.UpdateMergeRequest(c.projectId, c.mergeId, &gitlab.UpdateMergeRequestOptions{
+ ReviewerIDs: &reviewerUpdateRequest.Ids,
+ })
+
+ if err != nil {
+ c.handleError(w, err, "Could not modify merge request reviewers", http.StatusBadRequest)
+ return
+ }
+
+ if res.StatusCode != http.StatusOK {
+ c.handleError(w, err, "Could not modify merge request reviewers", http.StatusBadRequest)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+
+ response := ReviewerUpdateResponse{
+ SuccessResponse: SuccessResponse{
+ Message: "Reviewers updated",
+ Status: http.StatusOK,
+ },
+ Reviewers: mr.Reviewers,
+ }
+
+ json.NewEncoder(w).Encode(response)
+
+}
diff --git a/lua/gitlab/assignees_and_reviewers.lua b/lua/gitlab/assignees_and_reviewers.lua
new file mode 100644
index 0000000..af7951e
--- /dev/null
+++ b/lua/gitlab/assignees_and_reviewers.lua
@@ -0,0 +1,71 @@
+local u = require("gitlab.utils")
+local job = require("gitlab.job")
+local state = require("gitlab.state")
+local M = {}
+
+M.add_assignee = function()
+ M.add_popup('assignee')
+end
+
+M.delete_assignee = function()
+ M.delete_popup('assignee')
+end
+
+M.add_reviewer = function()
+ M.add_popup('reviewer')
+end
+
+M.delete_reviewer = function()
+ M.delete_popup('reviewer')
+end
+
+M.add_popup = function(type)
+ local plural = type .. 's'
+ local current = state.INFO[plural]
+ local eligible = M.filter_eligible(state.PROJECT_MEMBERS, current)
+ vim.ui.select(eligible, {
+ prompt = 'Choose ' .. type .. ' to add',
+ format_item = function(user)
+ return user.username .. " (" .. user.name .. ")"
+ end
+ }, function(choice)
+ if not choice then return end
+ local current_ids = u.extract(current, 'id')
+ table.insert(current_ids, choice.id)
+ local json = vim.json.encode({ ids = current_ids })
+ job.run_job("mr/" .. type, "PUT", json, function(data)
+ vim.notify(data.message, vim.log.levels.INFO)
+ state.INFO[plural] = data[plural]
+ end)
+ end)
+end
+
+M.delete_popup = function(type)
+ local plural = type .. 's'
+ local current = state.INFO[plural]
+ vim.ui.select(current, {
+ prompt = 'Choose ' .. type .. ' to delete',
+ format_item = function(user)
+ return user.username .. " (" .. user.name .. ")"
+ end
+ }, function(choice)
+ if not choice then return end
+ local ids = u.extract(M.filter_eligible(current, { choice }), 'id')
+ local json = vim.json.encode({ ids = ids })
+ job.run_job("mr/" .. type, "PUT", json, function(data)
+ vim.notify(data.message, vim.log.levels.INFO)
+ state.INFO[plural] = data[plural]
+ end)
+ end)
+end
+
+M.filter_eligible = function(current, to_remove)
+ local ids = u.extract(to_remove, 'id')
+ local res = {}
+ for _, member in ipairs(current) do
+ if not u.contains(ids, member.id) then table.insert(res, member) end
+ end
+ return res
+end
+
+return M
diff --git a/lua/gitlab/init.lua b/lua/gitlab/init.lua
index 5838821..845cd5d 100644
--- a/lua/gitlab/init.lua
+++ b/lua/gitlab/init.lua
@@ -1,13 +1,18 @@
-local state = require("gitlab.state")
-local discussions = require("gitlab.discussions")
-local summary = require("gitlab.summary")
-local keymaps = require("gitlab.keymaps")
-local comment = require("gitlab.comment")
-local job = require("gitlab.job")
-local u = require("gitlab.utils")
+local state = require("gitlab.state")
+local discussions = require("gitlab.discussions")
+local summary = require("gitlab.summary")
+local assignees_and_reviewers = require("gitlab.assignees_and_reviewers")
+local keymaps = require("gitlab.keymaps")
+local comment = require("gitlab.comment")
+local job = require("gitlab.job")
+local u = require("gitlab.utils")
--- Ensures the plugin's state is initialized prior to running other calls. This state contains the basic information about the current merge request, like description, author, etc
-local ensureState = function(callback)
+-- Function names prefixed with "ensure" will ensure the plugin's state
+-- is initialized prior to running other calls. These functions run
+-- API calls if the state isn't initialized, which will set state containing
+-- information that's necessary for other API calls, like description,
+-- author, reviewer, etc.
+local ensureState = function(callback)
return function()
if type(state.INFO) ~= "table" then
job.run_job("info", "GET", nil, function(data)
@@ -20,20 +25,37 @@ local ensureState = function(callback)
end
end
+local ensureProjectMembers = function(callback)
+ return function()
+ if type(state.PROJECT_MEMBERS) ~= "table" then
+ job.run_job("members", "GET", nil, function(data)
+ state.PROJECT_MEMBERS = data.ProjectMembers
+ callback()
+ end)
+ else
+ callback()
+ end
+ end
+end
+
-- Root Module Scope
-local M = {}
-M.summary = ensureState(summary.summary)
-M.approve = ensureState(job.approve)
-M.revoke = ensureState(job.revoke)
-M.create_comment = ensureState(comment.create_comment)
-M.list_discussions = ensureState(discussions.list_discussions)
-M.edit_comment = ensureState(comment.edit_comment)
-M.delete_comment = ensureState(comment.delete_comment)
-M.reply = ensureState(discussions.reply)
-M.state = state
+local M = {}
+M.summary = ensureState(summary.summary)
+M.approve = ensureState(job.approve)
+M.revoke = ensureState(job.revoke)
+M.create_comment = ensureState(comment.create_comment)
+M.list_discussions = ensureState(discussions.list_discussions)
+M.edit_comment = ensureState(comment.edit_comment)
+M.delete_comment = ensureState(comment.delete_comment)
+M.add_reviewer = ensureProjectMembers(ensureState(assignees_and_reviewers.add_reviewer))
+M.delete_reviewer = ensureProjectMembers(ensureState(assignees_and_reviewers.delete_reviewer))
+M.add_assignee = ensureProjectMembers(ensureState(assignees_and_reviewers.add_assignee))
+M.delete_assignee = ensureProjectMembers(ensureState(assignees_and_reviewers.delete_assignee))
+M.reply = ensureState(discussions.reply)
+M.state = state
-- Builds the binary (if not built); starts the Go server; sets the keymaps
-M.setup = function(args)
+M.setup = function(args)
if args == nil then args = {} end
local file_path = u.current_file_path()
local parent_dir = vim.fn.fnamemodify(file_path, ":h:h:h:h")
@@ -75,7 +97,7 @@ M.setup = function(args)
end
-- Builds the Go binary
-M.build = function()
+M.build = function()
local command = string.format("cd %s && make", state.BIN_PATH)
local installCode = os.execute(command .. "> /dev/null")
if installCode ~= 0 then
@@ -87,7 +109,7 @@ end
-- Initializes state for the project based on the arguments
-- provided in the `.gitlab.nvim` file per project, and the args provided in the setup function
-M.setPluginConfiguration = function(args)
+M.setPluginConfiguration = function(args)
local config_file_path = vim.fn.getcwd() .. "/.gitlab.nvim"
local config_file_content = u.read_file(config_file_path)
if config_file_content == nil then
diff --git a/lua/gitlab/summary.lua b/lua/gitlab/summary.lua
index 59cddfc..31d1feb 100644
--- a/lua/gitlab/summary.lua
+++ b/lua/gitlab/summary.lua
@@ -27,7 +27,7 @@ end
M.edit_description = function(text)
local jsonTable = { description = text }
local json = vim.json.encode(jsonTable)
- job.run_job("mr", "PUT", json, function(data)
+ job.run_job("mr/description", "PUT", json, function(data)
vim.notify(data.message, vim.log.levels.INFO)
state.INFO.description = data.mr.description
end)
diff --git a/lua/gitlab/utils/init.lua b/lua/gitlab/utils/init.lua
index 5f9d062..b780fc7 100644
--- a/lua/gitlab/utils/init.lua
+++ b/lua/gitlab/utils/init.lua
@@ -244,15 +244,6 @@ local current_file_path = function()
return vim.fn.fnamemodify(path, ':p')
end
--- Function to join two tables
-local function join_tables(table1, table2)
- for _, value in ipairs(table2) do
- table.insert(table1, value)
- end
-
- return table1
-end
-
local random = math.random
local function uuid()
local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
@@ -266,6 +257,36 @@ local attach_uuid = function(str)
return { text = str, id = uuid() }
end
+local join_tables = function(table1, table2)
+ for _, value in ipairs(table2) do
+ table.insert(table1, value)
+ end
+
+ return table1
+end
+
+local contains = function(array, search_value)
+ for _, value in ipairs(array) do
+ if value == search_value then
+ return true
+ end
+ end
+ return false
+end
+
+local extract = function(t, property)
+ local resultTable = {}
+ for _, value in ipairs(t) do
+ if value[property] then
+ table.insert(resultTable, value[property])
+ end
+ end
+ return resultTable
+end
+
+
+M.extract = extract
+M.contains = contains
M.attach_uuid = attach_uuid
M.join_tables = join_tables
M.get_relative_file_path = get_relative_file_path