Skip to content
Merged
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dc5c07de-4ae6-4335
- [Usage](#usage)
- [The summary command](#summary)
- [Reviewing Diffs](#reviewing-diffs)
- [Merging](#merging-an-mr)
- [Discussions and Notes](#discussions-and-notes)
- [Discussion signs and diagnostics](#discussion-signs-and-diagnostics)
- [Uploading Files](#uploading-files)
Expand Down Expand Up @@ -131,6 +132,7 @@ require("gitlab").setup({
note = nil,
pipeline = nil,
reply = nil,
squash_message = nil,
},
discussion_tree = { -- The discussion tree that holds all comments
auto_open = true, -- Automatically open when the reviewer is opened
Expand Down Expand Up @@ -210,6 +212,10 @@ require("gitlab").setup({
success = "✓",
failed = "",
},
merge = { -- The default behaviors when merging an MR, see "Merging an MR"
squash = false,
delete_branch = false,
},
colors = {
discussion_tree = {
username = "Keyword",
Expand Down Expand Up @@ -258,6 +264,19 @@ require("gitlab").create_comment_suggestion()

For suggesting changes you can use `create_comment_suggestion` in visual mode which works similar to `create_multiline_comment` but prefills the comment window with Gitlab's [suggest changes](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html) code block with prefilled code from the visual selection.

### Merging an MR

The `merge` action will merge an MR. The MR must be in a "mergeable" state for this command to work.

```lua
require("gitlab").merge()
require("gitlab").merge({ squash = false, delete_branch = false })
```

You can configure default behaviors via the setup function, values passed into this function will override the defaults.

If you enable `squash` you will be prompted for a squash message. To use the default message, leave the popup empty. Use the `settings.popup.perform_action` to merge the MR with your message.

### Discussions and Notes

Gitlab groups threads of comments together into "discussions."
Expand Down Expand Up @@ -391,6 +410,7 @@ vim.keymap.set("n", "glra", gitlab.add_reviewer)
vim.keymap.set("n", "glrd", gitlab.delete_reviewer)
vim.keymap.set("n", "glp", gitlab.pipeline)
vim.keymap.set("n", "glo", gitlab.open_in_browser)
vim.keymap.set("n", "glM", gitlab.merge)
```

## Troubleshooting
Expand Down
71 changes: 71 additions & 0 deletions cmd/merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"encoding/json"
"io"
"net/http"

"github.com/xanzy/go-gitlab"
)

type AcceptMergeRequestRequest struct {
Squash bool `json:"squash"`
SquashMessage string `json:"squash_message"`
DeleteBranch bool `json:"delete_branch"`
}

/* acceptAndMergeHandler merges a given merge request into the target branch */
func (a *api) acceptAndMergeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
if r.Method != http.MethodPost {
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
return
}

body, err := io.ReadAll(r.Body)
if err != nil {
handleError(w, err, "Could not read request body", http.StatusBadRequest)
return
}

var acceptAndMergeRequest AcceptMergeRequestRequest
err = json.Unmarshal(body, &acceptAndMergeRequest)
if err != nil {
handleError(w, err, "Could not unmarshal request body", http.StatusBadRequest)
return
}

opts := gitlab.AcceptMergeRequestOptions{
Squash: &acceptAndMergeRequest.Squash,
ShouldRemoveSourceBranch: &acceptAndMergeRequest.DeleteBranch,
}

if acceptAndMergeRequest.SquashMessage != "" {
opts.SquashCommitMessage = &acceptAndMergeRequest.SquashMessage
}

_, res, err := a.client.AcceptMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opts)

if err != nil {
handleError(w, err, "Could not merge MR", http.StatusInternalServerError)
return
}

if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/merge"}, "Could not merge MR", res.StatusCode)
return
}

response := SuccessResponse{
Status: http.StatusOK,
Message: "MR merged successfully",
}

w.WriteHeader(http.StatusOK)

err = json.NewEncoder(w).Encode(response)
if err != nil {
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
}
}
52 changes: 52 additions & 0 deletions cmd/merge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"errors"
"net/http"
"testing"

"github.com/xanzy/go-gitlab"
)

func acceptAndMergeFn(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return &gitlab.MergeRequest{}, makeResponse(http.StatusOK), nil
}

func acceptAndMergeFnErr(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return nil, nil, errors.New("Some error from Gitlab")
}

func acceptAndMergeNon200(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return nil, makeResponse(http.StatusSeeOther), nil
}

func TestAcceptAndMergeHandler(t *testing.T) {
t.Run("Accepts and merges a merge request", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/merge", AcceptMergeRequestRequest{})
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFn})
data := serveRequest(t, server, request, SuccessResponse{})
assert(t, data.Message, "MR merged successfully")
assert(t, data.Status, http.StatusOK)
})

t.Run("Disallows non-POST methods", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/merge", AcceptMergeRequestRequest{})
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFn})
data := serveRequest(t, server, request, ErrorResponse{})
checkBadMethod(t, *data, http.MethodPost)
})

t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/merge", AcceptMergeRequestRequest{})
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFnErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not merge MR")
})

t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/merge", AcceptMergeRequestRequest{})
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not merge MR", "/merge")
})
}
1 change: 1 addition & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func createRouterAndApi(client ClientInterface, optFuncs ...optFunc) (*http.Serv
m.HandleFunc("/shutdown", a.shutdownHandler)
m.HandleFunc("/approve", a.approveHandler)
m.HandleFunc("/comment", a.commentHandler)
m.HandleFunc("/merge", a.acceptAndMergeHandler)
m.HandleFunc("/discussions/list", a.listDiscussionsHandler)
m.HandleFunc("/discussions/resolve", a.discussionsResolveHandler)
m.HandleFunc("/info", a.infoHandler)
Expand Down
5 changes: 5 additions & 0 deletions cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The FakeHandlerClient is used to create a fake gitlab client for testing our han
type fakeClient struct {
getMergeRequestFn func(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
updateMergeRequestFn func(pid interface{}, mergeRequest int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
acceptAndMergeFn func(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
unapprorveMergeRequestFn func(pid interface{}, mr int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
uploadFile func(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error)
getMergeRequestDiffVersions func(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestDiffVersionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequestDiffVersion, *gitlab.Response, error)
Expand All @@ -46,6 +47,10 @@ type Author struct {
WebURL string `json:"web_url"`
}

func (f fakeClient) AcceptMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return f.acceptAndMergeFn(pid, mergeRequest, opt, options...)
}

func (f fakeClient) GetMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return f.getMergeRequestFn(pid, mergeRequest, opt, options...)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (e InvalidRequestError) Error() string {
/* The ClientInterface interface implements all the methods that our handlers need */
type ClientInterface interface {
GetMergeRequest(pid interface{}, mr int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
AcceptMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
UpdateMergeRequest(pid interface{}, mr int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
UploadFile(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error)
GetMergeRequestDiffVersions(pid interface{}, mr int, opt *gitlab.GetMergeRequestDiffVersionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequestDiffVersion, *gitlab.Response, error)
Expand Down
55 changes: 55 additions & 0 deletions lua/gitlab/actions/merge.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
local u = require("gitlab.utils")
local Popup = require("nui.popup")
local state = require("gitlab.state")
local job = require("gitlab.job")
local reviewer = require("gitlab.reviewer")

local M = {}

local function create_squash_message_popup()
return Popup(u.create_popup_state("Squash Commit Message", state.settings.popup.squash_message))
end

---@class MergeOpts
---@field delete_branch boolean?
---@field squash boolean?
---@field squash_message string?

---@param opts MergeOpts
M.merge = function(opts)
local merge_body = { squash = state.settings.merge.squash, delete_branch = state.settings.merge.delete_branch }
if opts then
merge_body.squash = opts.squash ~= nil and opts.squash
merge_body.delete_branch = opts.delete_branch ~= nil and opts.delete_branch
end

if state.INFO.detailed_merge_status ~= "mergeable" then
u.notify(string.format("MR not mergeable, currently '%s'", state.INFO.detailed_merge_status), vim.log.levels.ERROR)
return
end

if merge_body.squash then
local squash_message_popup = create_squash_message_popup()
squash_message_popup:mount()
state.set_popup_keymaps(squash_message_popup, function(text)
M.confirm_merge(merge_body, text)
end)
else
M.confirm_merge(merge_body)
end
end

---@param merge_body MergeOpts
---@param squash_message string?
M.confirm_merge = function(merge_body, squash_message)
if squash_message ~= nil then
merge_body.squash_message = squash_message
end

job.run_job("/merge", "POST", merge_body, function(data)
reviewer.close()
u.notify(data.message, vim.log.levels.INFO)
end)
end

return M
8 changes: 7 additions & 1 deletion lua/gitlab/actions/summary.lua
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ M.build_info_lines = function()
author = { title = "Author", content = "@" .. info.author.username .. " (" .. info.author.name .. ")" },
created_at = { title = "Created", content = u.format_to_local(info.created_at, vim.fn.strftime("%z")) },
updated_at = { title = "Updated", content = u.format_to_local(info.updated_at, vim.fn.strftime("%z")) },
merge_status = { title = "Status", content = info.detailed_merge_status },
detailed_merge_status = { title = "Status", content = info.detailed_merge_status },
draft = { title = "Draft", content = (info.draft and "Yes" or "No") },
conflicts = { title = "Merge Conflicts", content = (info.has_conflicts and "Yes" or "No") },
assignees = { title = "Assignees", content = u.make_readable_list(info.assignees, "name") },
Expand All @@ -138,6 +138,9 @@ M.build_info_lines = function()

local longest_used = ""
for _, v in ipairs(state.settings.info.fields) do
if v == "merge_status" then
v = "detailed_merge_status"
end -- merge_status was deprecated, see https://gitlab.com/gitlab-org/gitlab/-/issues/3169#note_1162532204
local title = options[v].title
if string.len(title) > string.len(longest_used) then
longest_used = title
Expand All @@ -151,6 +154,9 @@ M.build_info_lines = function()

local lines = {}
for _, v in ipairs(state.settings.info.fields) do
if v == "merge_status" then
v = "detailed_merge_status"
end
local row = options[v]
local line = "* " .. row.title .. row_offset(row.title)
if type(row.content) == "function" then
Expand Down
7 changes: 6 additions & 1 deletion lua/gitlab/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local server = require("gitlab.server")
local state = require("gitlab.state")
local reviewer = require("gitlab.reviewer")
local discussions = require("gitlab.actions.discussions")
local merge = require("gitlab.actions.merge")
local summary = require("gitlab.actions.summary")
local assignees_and_reviewers = require("gitlab.actions.assignees_and_reviewers")
local comment = require("gitlab.actions.comment")
Expand All @@ -27,7 +28,7 @@ return {
discussions.initialize_discussions() -- place signs / diagnostics for discussions in reviewer
end,
-- Global Actions 🌎
summary = async.sequence({ info }, summary.summary),
summary = async.sequence({ u.merge(info, { refresh = true }) }, summary.summary),
approve = async.sequence({ info }, approvals.approve),
revoke = async.sequence({ info }, approvals.revoke),
add_reviewer = async.sequence({ info, project_members }, assignees_and_reviewers.add_reviewer),
Expand All @@ -42,7 +43,11 @@ return {
review = async.sequence({ u.merge(info, { refresh = true }), revisions }, function()
reviewer.open()
end),
close_review = function()
reviewer.close()
end,
pipeline = async.sequence({ info }, pipeline.open),
merge = async.sequence({ u.merge(info, { refresh = true }) }, merge.merge),
-- Discussion Tree Actions 🌴
toggle_discussions = async.sequence({ info }, discussions.toggle),
edit_comment = async.sequence({ info }, discussions.edit_comment),
Expand Down
6 changes: 6 additions & 0 deletions lua/gitlab/reviewer/diffview.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ M.open = function()
end
end

M.close = function()
vim.cmd("DiffviewClose")
local discussions = require("gitlab.actions.discussions")
discussions.close()
end

M.jump = function(file_name, new_line, old_line)
if M.tabnr == nil then
u.notify("Can't jump to Diffvew. Is it open?", vim.log.levels.ERROR)
Expand Down
3 changes: 3 additions & 0 deletions lua/gitlab/reviewer/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ M.init = function()
M.open = reviewer.open
-- Opens the reviewer window

M.close = reviewer.close
-- Closes the reviewer and cleans up

M.jump = reviewer.jump
-- Jumps to the location provided in the reviewer window
-- Parameters:
Expand Down
5 changes: 5 additions & 0 deletions lua/gitlab/state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ M.settings = {
note = nil,
help = nil,
pipeline = nil,
squash_message = nil,
},
discussion_tree = {
auto_open = true,
Expand Down Expand Up @@ -66,6 +67,10 @@ M.settings = {
return " " .. discussions_content .. " %#Comment#| " .. notes_content
end,
},
merge = {
squash = false,
delete_branch = false,
},
info = {
enabled = true,
horizontal = false,
Expand Down