Feat: Create Merge Request (#149)
- Adds the ability to create MRs to the plugin - Adds the ability to jump to specific discussions/notes in the browser - Fixes stale icons - Adds debug keybinding for discussion tree for developers
This commit is contained in:
committed by
GitHub
parent
35f0bc16a5
commit
37a53842d0
85
README.md
85
README.md
@@ -2,14 +2,13 @@
|
||||
|
||||
This Neovim plugin is designed to make it easy to review Gitlab MRs from within the editor. This means you can do things like:
|
||||
|
||||
- Read and Edit an MR description
|
||||
- Approve or revoke approval for an MR
|
||||
- Create, approve, and merge MRs for the current branch
|
||||
- Read and edit an MR description
|
||||
- Add or remove reviewers and assignees
|
||||
- Resolve, reply to, and unresolve discussion threads
|
||||
- Create, edit, delete, and reply to comments on an MR
|
||||
- View and Manage Pipeline Jobs
|
||||
|
||||
And a lot more!
|
||||
- Create, edit, delete, and reply to comments
|
||||
- View and manage pipeline Jobs
|
||||
- Upload files, jump to the browser, and a lot more!
|
||||
|
||||
https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dc5c07de-4ae6-4335-afe1-d554e3804372
|
||||
|
||||
@@ -21,13 +20,14 @@ https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dc5c07de-4ae6-4335
|
||||
- [Connecting to Gitlab](#connecting-to-gitlab)
|
||||
- [Configuring the Plugin](#configuring-the-plugin)
|
||||
- [Usage](#usage)
|
||||
- [The summary command](#summary)
|
||||
- [Reviewing Diffs](#reviewing-diffs)
|
||||
- [The Summary view](#the-summary-view)
|
||||
- [Reviewing an MR](#reviewing-an-mr)
|
||||
- [Merging](#merging-an-mr)
|
||||
- [Discussions and Notes](#discussions-and-notes)
|
||||
- [Discussion signs and diagnostics](#discussion-signs-and-diagnostics)
|
||||
- [Signs and Diagnostics](#signs-and-diagnostics)
|
||||
- [Uploading Files](#uploading-files)
|
||||
- [MR Approvals](#mr-approvals)
|
||||
- [Approvals](#mr-approvals)
|
||||
- [Creating an MR](#creating-an-mr)
|
||||
- [Pipelines](#pipelines)
|
||||
- [Reviewers and Assignees](#reviewers-and-assignees)
|
||||
- [Restarting or Shutting down](#restarting-or-shutting-down)
|
||||
@@ -147,15 +147,16 @@ require("gitlab").setup({
|
||||
toggle_node = "t", -- Opens or closes the discussion
|
||||
toggle_resolved = "p" -- Toggles the resolved status of the whole discussion
|
||||
position = "left", -- "top", "right", "bottom" or "left"
|
||||
open_in_browser = "b" -- Jump to the URL of the current note/discussion
|
||||
size = "20%", -- Size of split
|
||||
relative = "editor", -- Position of tree split relative to "editor" or "window"
|
||||
resolved = '✓', -- Symbol to show next to resolved discussions
|
||||
unresolved = '✖', -- Symbol to show next to unresolved discussions
|
||||
unresolved = '-', -- Symbol to show next to unresolved discussions
|
||||
tree_type = "simple", -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
|
||||
winbar = nil -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
|
||||
-- If using lualine, please add "gitlab" to disabled file types, otherwise you will not see the winbar.
|
||||
},
|
||||
info = { -- Show additional fields in the summary pane
|
||||
info = { -- Show additional fields in the summary view
|
||||
enabled = true,
|
||||
horizontal = false, -- Display metadata to the left of the summary rather than underneath
|
||||
fields = { -- The fields listed here will be displayed, in whatever order you choose
|
||||
@@ -202,13 +203,13 @@ require("gitlab").setup({
|
||||
display_opts = {}, -- see opts in vim.diagnostic.set
|
||||
},
|
||||
pipeline = {
|
||||
created = "",
|
||||
created = "",
|
||||
pending = "",
|
||||
preparing = "",
|
||||
scheduled = "",
|
||||
running = "ﰌ",
|
||||
canceled = "ﰸ",
|
||||
skipped = "ﰸ",
|
||||
running = "",
|
||||
canceled = "↪",
|
||||
skipped = "↪",
|
||||
success = "✓",
|
||||
failed = "",
|
||||
},
|
||||
@@ -216,6 +217,10 @@ require("gitlab").setup({
|
||||
squash = false,
|
||||
delete_branch = false,
|
||||
},
|
||||
create_mr = {
|
||||
target = nil, -- Default branch to target when creating an MR
|
||||
template_file = nil, -- Default MR template in .gitlab/merge_request_templates
|
||||
},
|
||||
colors = {
|
||||
discussion_tree = {
|
||||
username = "Keyword",
|
||||
@@ -239,7 +244,7 @@ git checkout feature-branch
|
||||
|
||||
Then open Neovim. To begin, try running the `summary` command or the `review` command.
|
||||
|
||||
### Summary
|
||||
### The Summary view
|
||||
|
||||
The `summary` action will open the MR title and description.
|
||||
|
||||
@@ -251,7 +256,7 @@ After editing the description or title, you may save your changes via the `setti
|
||||
|
||||
By default this plugin will also show additional metadata about the MR in a separate pane underneath the description. This can be disabled, and these fields can be reordered or removed. Please see the `settings.info` section of the configuration.
|
||||
|
||||
### Reviewing Diffs
|
||||
### Reviewing an MR
|
||||
|
||||
The `review` action will open a diff of the changes. You can leave comments using the `create_comment` action. In visual mode, add multiline comments with the `create_multiline_comment` command, and add suggested changes with the `create_comment_suggestion` command.
|
||||
|
||||
@@ -264,19 +269,6 @@ 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."
|
||||
@@ -297,7 +289,7 @@ If you'd like to create a note in an MR (like a comment, but not linked to a spe
|
||||
require("gitlab").create_note()
|
||||
```
|
||||
|
||||
### Discussion signs and diagnostics
|
||||
### Signs and diagnostics
|
||||
|
||||
By default when reviewing files you will see signs and diagnostics (if enabled in configuration). When cursor is on diagnostic line you can view discussion thread by using `vim.diagnostic.show`. You can also jump to discussion tree where you can reply, edit or delete discussion.
|
||||
|
||||
@@ -333,6 +325,32 @@ require("gitlab").approve()
|
||||
require("gitlab").revoke()
|
||||
```
|
||||
|
||||
### 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 the `merge` action 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.
|
||||
|
||||
|
||||
### Creating an MR
|
||||
|
||||
To create an MR for the current branch, make sure you have the branch checked out. Then, use the `create_mr` action.
|
||||
|
||||
```lua
|
||||
require("gitlab").create_mr()
|
||||
require("gitlab").create_mr({ target = "main" })
|
||||
require("gitlab").create_mr({ target = "main", template_file = "my-template.md" })
|
||||
```
|
||||
|
||||
You can configure default behaviors via the setup function, values passed into the `create_mr` action will override your defaults.
|
||||
|
||||
### Pipelines
|
||||
|
||||
You can view the status of the pipeline for the current MR with the `pipeline` action.
|
||||
@@ -401,6 +419,7 @@ vim.keymap.set("n", "glR", gitlab.revoke)
|
||||
vim.keymap.set("n", "glc", gitlab.create_comment)
|
||||
vim.keymap.set("v", "glc", gitlab.create_multiline_comment)
|
||||
vim.keymap.set("v", "glC", gitlab.create_comment_suggestion)
|
||||
vim.keymap.set("n", "glO", gitlab.create_mr)
|
||||
vim.keymap.set("n", "glm", gitlab.move_to_discussion_tree_from_diagnostic)
|
||||
vim.keymap.set("n", "gln", gitlab.create_note)
|
||||
vim.keymap.set("n", "gld", gitlab.toggle_discussions)
|
||||
@@ -427,5 +446,5 @@ This plugin uses a Go server to reach out to Gitlab. It's possible that somethin
|
||||
The easiest way to debug what's going wrong is to turn on the `debug` options in your setup function. This will allow you to see requests leaving the Go server, and the responses coming back from Gitlab. Once the server is running, you can also interact with the Go server like any other process:
|
||||
|
||||
```
|
||||
curl --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" localhost:21036/info
|
||||
curl --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" localhost:21036/mr/info
|
||||
```
|
||||
|
||||
@@ -22,7 +22,7 @@ func (a *api) approveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/approve"}, "Could not approve merge request", res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/approve"}, "Could not approve merge request", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func approveMergeRequestErr(pid interface{}, mr int, opt *gitlab.ApproveMergeReq
|
||||
|
||||
func TestApproveHandler(t *testing.T) {
|
||||
t.Run("Approves merge request", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/approve", nil)
|
||||
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
|
||||
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequest})
|
||||
data := serveRequest(t, server, request, SuccessResponse{})
|
||||
assert(t, data.Message, "Approved MR")
|
||||
@@ -30,23 +30,23 @@ func TestApproveHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Disallows non-POST method", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPut, "/approve", nil)
|
||||
request := makeRequest(t, http.MethodPut, "/mr/approve", nil)
|
||||
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequest})
|
||||
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, "/approve", nil)
|
||||
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
|
||||
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequestErr})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkErrorFromGitlab(t, *data, "Could not approve merge request")
|
||||
})
|
||||
|
||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/approve", nil)
|
||||
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
|
||||
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequestNon200})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not approve merge request", "/approve")
|
||||
checkNon200(t, *data, "Could not approve merge request", "/mr/approve")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (a *api) attachmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/mr/attachment"}, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FileName), res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/attachment"}, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FileName), res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ func withMockFileReader(a *api) error {
|
||||
|
||||
func TestAttachmentHandler(t *testing.T) {
|
||||
t.Run("Returns 200-status response after upload", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||
request := makeRequest(t, http.MethodPost, "/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFile}, withMockFileReader)
|
||||
data := serveRequest(t, router, request, AttachmentResponse{})
|
||||
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||
@@ -44,23 +44,23 @@ func TestAttachmentHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Disallows non-POST method", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPut, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||
request := makeRequest(t, http.MethodPut, "/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFile}, withMockFileReader)
|
||||
data := serveRequest(t, router, request, ErrorResponse{})
|
||||
checkBadMethod(t, *data, http.MethodPost)
|
||||
})
|
||||
|
||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||
request := makeRequest(t, http.MethodPost, "/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFileErr}, withMockFileReader)
|
||||
data := serveRequest(t, router, request, ErrorResponse{})
|
||||
checkErrorFromGitlab(t, *data, "Could not upload some_file_name to Gitlab")
|
||||
})
|
||||
|
||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||
request := makeRequest(t, http.MethodPost, "/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFileNon200}, withMockFileReader)
|
||||
data := serveRequest(t, router, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not upload some_file_name to Gitlab", "/mr/attachment")
|
||||
checkNon200(t, *data, "Could not upload some_file_name to Gitlab", "/attachment")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/xanzy/go-gitlab"
|
||||
@@ -90,7 +89,7 @@ func initGitlabClient() (error, *Client) {
|
||||
}
|
||||
}
|
||||
|
||||
/* initProjectSettings fetch the project ID and merge request ID using the client. */
|
||||
/* initProjectSettings fetch the project ID using the client */
|
||||
func initProjectSettings(c *Client, gitInfo GitProjectInfo) (error, *ProjectInfo) {
|
||||
|
||||
opt := gitlab.GetProjectOptions{}
|
||||
@@ -109,31 +108,10 @@ func initProjectSettings(c *Client, gitInfo GitProjectInfo) (error, *ProjectInfo
|
||||
|
||||
projectId := fmt.Sprint(project.ID)
|
||||
|
||||
options := gitlab.ListProjectMergeRequestsOptions{
|
||||
Scope: gitlab.String("all"),
|
||||
State: gitlab.String("opened"),
|
||||
SourceBranch: &gitInfo.BranchName,
|
||||
}
|
||||
|
||||
mergeRequests, _, err := c.ListProjectMergeRequests(projectId, &options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to list merge requests: %w", err), nil
|
||||
}
|
||||
|
||||
if len(mergeRequests) == 0 {
|
||||
return errors.New("No merge requests found"), nil
|
||||
}
|
||||
|
||||
mergeId := strconv.Itoa(mergeRequests[0].IID)
|
||||
mergeIdInt, err := strconv.Atoi(mergeId)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
||||
return nil, &ProjectInfo{
|
||||
MergeId: mergeIdInt,
|
||||
ProjectId: projectId,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* handleError is a utililty handler that returns errors to the client along with their statuses and messages */
|
||||
|
||||
@@ -95,7 +95,7 @@ func (a *api) deleteComment(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/comment"}, "Could not delete comment", res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/comment"}, "Could not delete comment", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ func (a *api) postComment(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/comment"}, "Could not create discussion", res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/comment"}, "Could not create discussion", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ func (a *api) editComment(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/comment"}, "Could not update comment", res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/comment"}, "Could not update comment", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func createMergeRequestDiscussionErr(pid interface{}, mergeRequest int, opt *git
|
||||
|
||||
func TestPostComment(t *testing.T) {
|
||||
t.Run("Creates a new note (unlinked comment)", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
||||
data := serveRequest(t, server, request, CommentResponse{})
|
||||
assert(t, data.SuccessResponse.Message, "Note created successfully")
|
||||
@@ -30,7 +30,7 @@ func TestPostComment(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Creates a new comment", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{FileName: "some_file.txt"})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{FileName: "some_file.txt"})
|
||||
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
||||
data := serveRequest(t, server, request, CommentResponse{})
|
||||
assert(t, data.SuccessResponse.Message, "Comment created successfully")
|
||||
@@ -38,7 +38,7 @@ func TestPostComment(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Creates a new multiline comment", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{
|
||||
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{
|
||||
FileName: "some_file.txt",
|
||||
LineRange: &LineRange{
|
||||
StartRange: &LinePosition{}, /* These would have real data */
|
||||
@@ -52,17 +52,17 @@ func TestPostComment(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussionErr})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkErrorFromGitlab(t, *data, "Could not create discussion")
|
||||
})
|
||||
|
||||
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussionNon200})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not create discussion", "/comment")
|
||||
checkNon200(t, *data, "Could not create discussion", "/mr/comment")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func deleteMergeRequestDiscussionNoteNon200(pid interface{}, mergeRequest int, d
|
||||
|
||||
func TestDeleteComment(t *testing.T) {
|
||||
t.Run("Deletes a comment", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodDelete, "/comment", DeleteCommentRequest{})
|
||||
request := makeRequest(t, http.MethodDelete, "/mr/comment", DeleteCommentRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{deleteMergeRequestDiscussionNote: deleteMergeRequestDiscussionNote})
|
||||
data := serveRequest(t, server, request, CommentResponse{})
|
||||
assert(t, data.SuccessResponse.Message, "Comment deleted successfully")
|
||||
@@ -88,17 +88,17 @@ func TestDeleteComment(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodDelete, "/comment", DeleteCommentRequest{})
|
||||
request := makeRequest(t, http.MethodDelete, "/mr/comment", DeleteCommentRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{deleteMergeRequestDiscussionNote: deleteMergeRequestDiscussionNoteErr})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkErrorFromGitlab(t, *data, "Could not delete comment")
|
||||
})
|
||||
|
||||
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodDelete, "/comment", DeleteCommentRequest{})
|
||||
request := makeRequest(t, http.MethodDelete, "/mr/comment", DeleteCommentRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{deleteMergeRequestDiscussionNote: deleteMergeRequestDiscussionNoteNon200})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not delete comment", "/comment")
|
||||
checkNon200(t, *data, "Could not delete comment", "/mr/comment")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func updateMergeRequestDiscussionNoteNon200(pid interface{}, mergeRequest int, d
|
||||
|
||||
func TestEditComment(t *testing.T) {
|
||||
t.Run("Edits a comment", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPatch, "/comment", EditCommentRequest{})
|
||||
request := makeRequest(t, http.MethodPatch, "/mr/comment", EditCommentRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{updateMergeRequestDiscussionNote: updateMergeRequestDiscussionNote})
|
||||
data := serveRequest(t, server, request, CommentResponse{})
|
||||
assert(t, data.SuccessResponse.Message, "Comment updated successfully")
|
||||
@@ -124,16 +124,16 @@ func TestEditComment(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPatch, "/comment", EditCommentRequest{})
|
||||
request := makeRequest(t, http.MethodPatch, "/mr/comment", EditCommentRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{updateMergeRequestDiscussionNote: updateMergeRequestDiscussionNoteErr})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkErrorFromGitlab(t, *data, "Could not update comment")
|
||||
})
|
||||
|
||||
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPatch, "/comment", EditCommentRequest{})
|
||||
request := makeRequest(t, http.MethodPatch, "/mr/comment", EditCommentRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{updateMergeRequestDiscussionNote: updateMergeRequestDiscussionNoteNon200})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not update comment", "/comment")
|
||||
checkNon200(t, *data, "Could not update comment", "/mr/comment")
|
||||
})
|
||||
}
|
||||
|
||||
81
cmd/create_mr.go
Normal file
81
cmd/create_mr.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
type CreateMrRequest struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
TargetBranch string `json:"target_branch"`
|
||||
}
|
||||
|
||||
/* createMr creates a merge request */
|
||||
func (a *api) createMr(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 createMrRequest CreateMrRequest
|
||||
err = json.Unmarshal(body, &createMrRequest)
|
||||
if err != nil {
|
||||
handleError(w, err, "Could not unmarshal request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if createMrRequest.Title == "" {
|
||||
handleError(w, errors.New("Title cannot be empty"), "Could not create MR", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if createMrRequest.TargetBranch == "" {
|
||||
handleError(w, errors.New("Target branch cannot be empty"), "Could not create MR", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
opts := gitlab.CreateMergeRequestOptions{
|
||||
Title: &createMrRequest.Title,
|
||||
Description: &createMrRequest.Description,
|
||||
TargetBranch: &createMrRequest.TargetBranch,
|
||||
SourceBranch: &a.gitInfo.BranchName,
|
||||
}
|
||||
|
||||
_, res, err := a.client.CreateMergeRequest(a.projectInfo.ProjectId, &opts)
|
||||
|
||||
if err != nil {
|
||||
handleError(w, err, "Could not create MR", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/create_mr"}, "Could not create MR", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
response := SuccessResponse{
|
||||
Status: http.StatusOK,
|
||||
Message: fmt.Sprintf("MR '%s' created", createMrRequest.Title),
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
err = json.NewEncoder(w).Encode(response)
|
||||
if err != nil {
|
||||
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
97
cmd/create_mr_test.go
Normal file
97
cmd/create_mr_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
func createMrFn(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||
return &gitlab.MergeRequest{}, makeResponse(http.StatusOK), nil
|
||||
}
|
||||
|
||||
func createMrFnErr(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||
return nil, nil, errors.New("Some error from Gitlab")
|
||||
}
|
||||
|
||||
func createMrFnNon200(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||
return nil, makeResponse(http.StatusSeeOther), nil
|
||||
}
|
||||
|
||||
func TestCreateMr(t *testing.T) {
|
||||
t.Run("Creates an MR", func(t *testing.T) {
|
||||
|
||||
body := CreateMrRequest{
|
||||
Title: "Some title",
|
||||
Description: "Some description",
|
||||
TargetBranch: "main",
|
||||
}
|
||||
|
||||
request := makeRequest(t, http.MethodPost, "/create_mr", body)
|
||||
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFn})
|
||||
data := serveRequest(t, server, request, SuccessResponse{})
|
||||
assert(t, data.Message, "MR 'Some title' created")
|
||||
assert(t, data.Status, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Disallows non-POST methods", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodGet, "/create_mr", CreateMrRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFn})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkBadMethod(t, *data, http.MethodPost)
|
||||
})
|
||||
|
||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||
body := CreateMrRequest{
|
||||
Title: "Some title",
|
||||
Description: "Some description",
|
||||
TargetBranch: "main",
|
||||
}
|
||||
request := makeRequest(t, http.MethodPost, "/create_mr", body)
|
||||
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFnErr})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkErrorFromGitlab(t, *data, "Could not create MR")
|
||||
})
|
||||
|
||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||
body := CreateMrRequest{
|
||||
Title: "Some title",
|
||||
Description: "Some description",
|
||||
TargetBranch: "main",
|
||||
}
|
||||
request := makeRequest(t, http.MethodPost, "/create_mr", body)
|
||||
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFnNon200})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not create MR", "/create_mr")
|
||||
})
|
||||
|
||||
t.Run("Handles missing titles", func(t *testing.T) {
|
||||
body := CreateMrRequest{
|
||||
Title: "",
|
||||
Description: "Some description",
|
||||
TargetBranch: "main",
|
||||
}
|
||||
request := makeRequest(t, http.MethodPost, "/create_mr", body)
|
||||
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFn})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
assert(t, data.Status, http.StatusBadRequest)
|
||||
assert(t, data.Message, "Could not create MR")
|
||||
assert(t, data.Details, "Title cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("Handles missing target branch", func(t *testing.T) {
|
||||
body := CreateMrRequest{
|
||||
Title: "Some title",
|
||||
Description: "Some description",
|
||||
TargetBranch: "",
|
||||
}
|
||||
request := makeRequest(t, http.MethodPost, "/create_mr", body)
|
||||
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFn})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
assert(t, data.Status, http.StatusBadRequest)
|
||||
assert(t, data.Message, "Could not create MR")
|
||||
assert(t, data.Details, "Target branch cannot be empty")
|
||||
})
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func (a *api) infoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/info"}, "Could not get project info", res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/info"}, "Could not get project info", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func getInfoErr(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsO
|
||||
|
||||
func TestInfoHandler(t *testing.T) {
|
||||
t.Run("Returns normal information", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodGet, "/info", nil)
|
||||
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
||||
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfo})
|
||||
data := serveRequest(t, server, request, InfoResponse{})
|
||||
assert(t, data.Info.Title, "Some Title")
|
||||
@@ -31,23 +31,23 @@ func TestInfoHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Disallows non-GET method", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/info", nil)
|
||||
request := makeRequest(t, http.MethodPost, "/mr/info", nil)
|
||||
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfo})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkBadMethod(t, *data, http.MethodGet)
|
||||
})
|
||||
|
||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodGet, "/info", nil)
|
||||
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
||||
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfoErr})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkErrorFromGitlab(t, *data, "Could not get project info")
|
||||
})
|
||||
|
||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodGet, "/info", nil)
|
||||
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
||||
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfoNon200})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not get project info", "/info")
|
||||
checkNon200(t, *data, "Could not get project info", "/mr/info")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (a *api) listDiscussionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/discussions/list"}, "Could not list discussions", res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/discussions/list"}, "Could not list discussions", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func listMergeRequestDiscussionsNon200(pid interface{}, mergeRequest int, opt *g
|
||||
|
||||
func TestListDiscussionsHandler(t *testing.T) {
|
||||
t.Run("Returns sorted discussions", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussions})
|
||||
data := serveRequest(t, server, request, DiscussionsResponse{})
|
||||
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
|
||||
@@ -59,7 +59,7 @@ func TestListDiscussionsHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Uses blacklist to filter unwanted authors", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer"}})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer"}})
|
||||
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussions})
|
||||
data := serveRequest(t, server, request, DiscussionsResponse{})
|
||||
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
|
||||
@@ -69,23 +69,23 @@ func TestListDiscussionsHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Disallows non-POST method", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPatch, "/discussions/list", DiscussionsRequest{})
|
||||
request := makeRequest(t, http.MethodPatch, "/mr/discussions/list", DiscussionsRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussions})
|
||||
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, "/discussions/list", DiscussionsRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussionsErr})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkErrorFromGitlab(t, *data, "Could not list discussions")
|
||||
})
|
||||
|
||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussionsNon200})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not list discussions", "/discussions/list")
|
||||
checkNon200(t, *data, "Could not list discussions", "/mr/discussions/list")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ func main() {
|
||||
log.Fatalf("Failed to initialize project settings: %v", err)
|
||||
}
|
||||
|
||||
startServer(client, projectInfo)
|
||||
startServer(client, projectInfo, gitInfo)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (a *api) acceptAndMergeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/merge"}, "Could not merge MR", res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/merge"}, "Could not merge MR", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func acceptAndMergeNon200(pid interface{}, mergeRequest int, opt *gitlab.AcceptM
|
||||
|
||||
func TestAcceptAndMergeHandler(t *testing.T) {
|
||||
t.Run("Accepts and merges a merge request", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/merge", AcceptMergeRequestRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFn})
|
||||
data := serveRequest(t, server, request, SuccessResponse{})
|
||||
assert(t, data.Message, "MR merged successfully")
|
||||
@@ -30,23 +30,23 @@ func TestAcceptAndMergeHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Disallows non-POST methods", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodGet, "/merge", AcceptMergeRequestRequest{})
|
||||
request := makeRequest(t, http.MethodGet, "/mr/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{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/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{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeNon200})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not merge MR", "/merge")
|
||||
checkNon200(t, *data, "Could not merge MR", "/mr/merge")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (a *api) replyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/reply"}, "Could not leave reply", res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/reply"}, "Could not leave reply", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func addMergeRequestDiscussionNoteNon200(pid interface{}, mergeRequest int, disc
|
||||
|
||||
func TestReplyHandler(t *testing.T) {
|
||||
t.Run("Sends a reply", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/reply", ReplyRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/reply", ReplyRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNote})
|
||||
data := serveRequest(t, server, request, ReplyResponse{})
|
||||
assert(t, data.SuccessResponse.Message, "Replied to comment")
|
||||
@@ -30,23 +30,23 @@ func TestReplyHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Disallows non-POST methods", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodGet, "/reply", ReplyRequest{})
|
||||
request := makeRequest(t, http.MethodGet, "/mr/reply", ReplyRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNote})
|
||||
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, "/reply", ReplyRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/reply", ReplyRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNoteErr})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkErrorFromGitlab(t, *data, "Could not leave reply")
|
||||
})
|
||||
|
||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||
request := makeRequest(t, http.MethodPost, "/reply", ReplyRequest{})
|
||||
request := makeRequest(t, http.MethodPost, "/mr/reply", ReplyRequest{})
|
||||
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNoteNon200})
|
||||
data := serveRequest(t, server, request, ErrorResponse{})
|
||||
checkNon200(t, *data, "Could not leave reply", "/reply")
|
||||
checkNon200(t, *data, "Could not leave reply", "/mr/reply")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (a *api) discussionsResolveHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/discussions/resolve"}, fmt.Sprintf("Could not %s discussion", friendlyName), res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/discussions/resolve"}, fmt.Sprintf("Could not %s discussion", friendlyName), res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func (a *api) revokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
handleError(w, GenericError{endpoint: "/revoke"}, "Could not revoke approval", res.StatusCode)
|
||||
handleError(w, GenericError{endpoint: "/mr/revoke"}, "Could not revoke approval", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,17 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
/*
|
||||
startSever starts the server and runs concurrent goroutines
|
||||
to handle potential shutdown requests and incoming HTTP requests.
|
||||
*/
|
||||
func startServer(client *Client, projectInfo *ProjectInfo) {
|
||||
func startServer(client *Client, projectInfo *ProjectInfo, gitInfo GitProjectInfo) {
|
||||
|
||||
m, a := createRouterAndApi(client,
|
||||
func(a *api) error {
|
||||
@@ -24,6 +27,10 @@ func startServer(client *Client, projectInfo *ProjectInfo) {
|
||||
func(a *api) error {
|
||||
a.fileReader = attachmentReader{}
|
||||
return nil
|
||||
},
|
||||
func(a *api) error {
|
||||
a.gitInfo = &gitInfo
|
||||
return nil
|
||||
})
|
||||
|
||||
l := createListener()
|
||||
@@ -73,6 +80,7 @@ with the client value, which is a go-gitlab client.
|
||||
type api struct {
|
||||
client ClientInterface
|
||||
projectInfo *ProjectInfo
|
||||
gitInfo *GitProjectInfo
|
||||
fileReader FileReader
|
||||
sigCh chan os.Signal
|
||||
}
|
||||
@@ -84,11 +92,13 @@ createRouterAndApi wires up the router and attaches all handlers to their respec
|
||||
iterates over all option functions to configure API fields such as the project information and default
|
||||
file reader functionality
|
||||
*/
|
||||
|
||||
func createRouterAndApi(client ClientInterface, optFuncs ...optFunc) (*http.ServeMux, api) {
|
||||
m := http.NewServeMux()
|
||||
a := api{
|
||||
client: client,
|
||||
projectInfo: &ProjectInfo{},
|
||||
gitInfo: &GitProjectInfo{},
|
||||
fileReader: nil,
|
||||
sigCh: make(chan os.Signal, 1),
|
||||
}
|
||||
@@ -101,24 +111,27 @@ func createRouterAndApi(client ClientInterface, optFuncs ...optFunc) (*http.Serv
|
||||
}
|
||||
}
|
||||
|
||||
m.Handle("/ping", http.HandlerFunc(pingHandler))
|
||||
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)
|
||||
m.HandleFunc("/mr/approve", a.withMr(a.approveHandler))
|
||||
m.HandleFunc("/mr/comment", a.withMr(a.commentHandler))
|
||||
m.HandleFunc("/mr/merge", a.withMr(a.acceptAndMergeHandler))
|
||||
m.HandleFunc("/mr/discussions/list", a.withMr(a.listDiscussionsHandler))
|
||||
m.HandleFunc("/mr/discussions/resolve", a.withMr(a.discussionsResolveHandler))
|
||||
m.HandleFunc("/mr/info", a.withMr(a.infoHandler))
|
||||
m.HandleFunc("/mr/assignee", a.withMr(a.assigneesHandler))
|
||||
m.HandleFunc("/mr/summary", a.withMr(a.summaryHandler))
|
||||
m.HandleFunc("/mr/reviewer", a.withMr(a.reviewersHandler))
|
||||
m.HandleFunc("/mr/revisions", a.withMr(a.revisionsHandler))
|
||||
m.HandleFunc("/mr/reply", a.withMr(a.replyHandler))
|
||||
m.HandleFunc("/mr/revoke", a.withMr(a.revokeHandler))
|
||||
|
||||
m.HandleFunc("/attachment", a.attachmentHandler)
|
||||
m.HandleFunc("/create_mr", a.createMr)
|
||||
m.HandleFunc("/job", a.jobHandler)
|
||||
m.HandleFunc("/mr/attachment", a.attachmentHandler)
|
||||
m.HandleFunc("/mr/assignee", a.assigneesHandler)
|
||||
m.HandleFunc("/mr/summary", a.summaryHandler)
|
||||
m.HandleFunc("/mr/reviewer", a.reviewersHandler)
|
||||
m.HandleFunc("/mr/revisions", a.revisionsHandler)
|
||||
m.HandleFunc("/pipeline/", a.pipelineHandler)
|
||||
m.HandleFunc("/project/members", a.projectMembersHandler)
|
||||
m.HandleFunc("/reply", a.replyHandler)
|
||||
m.HandleFunc("/revoke", a.revokeHandler)
|
||||
m.HandleFunc("/shutdown", a.shutdownHandler)
|
||||
|
||||
m.Handle("/ping", http.HandlerFunc(pingHandler))
|
||||
|
||||
return m, a
|
||||
}
|
||||
@@ -157,3 +170,41 @@ func createListener() (l net.Listener) {
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
/* withMr is a Middlware that gets the current merge request ID and attaches it to the projectInfo */
|
||||
func (a *api) withMr(f func(w http.ResponseWriter, r *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if a.projectInfo.MergeId != 0 {
|
||||
f(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
options := gitlab.ListProjectMergeRequestsOptions{
|
||||
Scope: gitlab.String("all"),
|
||||
State: gitlab.String("opened"),
|
||||
SourceBranch: &a.gitInfo.BranchName,
|
||||
}
|
||||
|
||||
mergeRequests, _, err := a.client.ListProjectMergeRequests(a.projectInfo.ProjectId, &options)
|
||||
if err != nil {
|
||||
handleError(w, fmt.Errorf("Failed to list merge requests: %w", err), "Failed to list merge requests", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(mergeRequests) == 0 {
|
||||
handleError(w, fmt.Errorf("No merge requests found for branch '%s'", a.gitInfo.BranchName), "No merge requests found", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mergeId := strconv.Itoa(mergeRequests[0].IID)
|
||||
mergeIdInt, err := strconv.Atoi(mergeId)
|
||||
if err != nil {
|
||||
handleError(w, err, "Could not convert merge ID to integer", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
a.projectInfo.MergeId = mergeIdInt
|
||||
f(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
39
cmd/test.go
39
cmd/test.go
@@ -18,6 +18,7 @@ The FakeHandlerClient is used to create a fake gitlab client for testing our han
|
||||
*/
|
||||
|
||||
type fakeClient struct {
|
||||
createMrFn func(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||
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)
|
||||
@@ -47,6 +48,10 @@ type Author struct {
|
||||
WebURL string `json:"web_url"`
|
||||
}
|
||||
|
||||
func (f fakeClient) CreateMergeRequest(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||
return f.createMrFn(pid, opt, options...)
|
||||
}
|
||||
|
||||
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...)
|
||||
}
|
||||
@@ -77,35 +82,6 @@ func (f fakeClient) ApproveMergeRequest(pid interface{}, mr int, opt *gitlab.App
|
||||
|
||||
func (f fakeClient) ListMergeRequestDiscussions(pid interface{}, mergeRequest int, opt *gitlab.ListMergeRequestDiscussionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Discussion, *gitlab.Response, error) {
|
||||
return f.listMergeRequestDiscussions(pid, mergeRequest, opt, options...)
|
||||
|
||||
// now := time.Now()
|
||||
// later := now.Add(time.Second * 100)
|
||||
//
|
||||
// discussions := []*gitlab.Discussion{
|
||||
// {
|
||||
// Notes: []*gitlab.Note{
|
||||
// {
|
||||
// CreatedAt: &now,
|
||||
// Type: "DiffNote",
|
||||
// Author: Author{
|
||||
// Username: "hcramer",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Notes: []*gitlab.Note{
|
||||
// {
|
||||
// CreatedAt: &later,
|
||||
// Type: "DiffNote",
|
||||
// Author: Author{
|
||||
// Username: "hcramer2",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// return discussions, makeResponse(200), nil
|
||||
}
|
||||
|
||||
func (f fakeClient) ResolveMergeRequestDiscussion(pid interface{}, mergeRequest int, discussion string, opt *gitlab.ResolveMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error) {
|
||||
@@ -144,6 +120,11 @@ func (f fakeClient) GetTraceFile(pid interface{}, jobID int, options ...gitlab.R
|
||||
return f.getTraceFile(pid, jobID, options...)
|
||||
}
|
||||
|
||||
/* This middleware function needs to return an ID for the rest of the handlers */
|
||||
func (f fakeClient) ListProjectMergeRequests(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||
return []*gitlab.MergeRequest{{ID: 1}}, &gitlab.Response{}, nil
|
||||
}
|
||||
|
||||
/* The assert function is a helper function used to check two comparables */
|
||||
func assert[T comparable](t *testing.T, got T, want T) {
|
||||
t.Helper()
|
||||
|
||||
@@ -35,6 +35,8 @@ func (e InvalidRequestError) Error() string {
|
||||
|
||||
/* The ClientInterface interface implements all the methods that our handlers need */
|
||||
type ClientInterface interface {
|
||||
CreateMergeRequest(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||
ListProjectMergeRequests(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error)
|
||||
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)
|
||||
|
||||
@@ -3,11 +3,11 @@ local job = require("gitlab.job")
|
||||
local M = {}
|
||||
|
||||
M.approve = function()
|
||||
job.run_job("/approve", "POST")
|
||||
job.run_job("/mr/approve", "POST")
|
||||
end
|
||||
|
||||
M.revoke = function()
|
||||
job.run_job("/revoke", "POST")
|
||||
job.run_job("/mr/revoke", "POST")
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -126,7 +126,7 @@ M.confirm_create_comment = function(text, range, unlinked)
|
||||
|
||||
if unlinked then
|
||||
local body = { comment = text }
|
||||
job.run_job("/comment", "POST", body, function(data)
|
||||
job.run_job("/mr/comment", "POST", body, function(data)
|
||||
u.notify("Note created!", vim.log.levels.INFO)
|
||||
discussions.add_discussion({ data = data, unlinked = true })
|
||||
discussions.refresh_discussion_data()
|
||||
@@ -152,7 +152,7 @@ M.confirm_create_comment = function(text, range, unlinked)
|
||||
line_range = reviewer_info.range_info,
|
||||
}
|
||||
|
||||
job.run_job("/comment", "POST", body, function(data)
|
||||
job.run_job("/mr/comment", "POST", body, function(data)
|
||||
u.notify("Comment created!", vim.log.levels.INFO)
|
||||
discussions.add_discussion({ data = data, unlinked = false })
|
||||
discussions.refresh_discussion_data()
|
||||
|
||||
321
lua/gitlab/actions/create_mr.lua
Normal file
321
lua/gitlab/actions/create_mr.lua
Normal file
@@ -0,0 +1,321 @@
|
||||
-- This module is responsible for creating am MR
|
||||
-- for the current branch
|
||||
local Layout = require("nui.layout")
|
||||
local Input = require("nui.input")
|
||||
local Popup = require("nui.popup")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
|
||||
---@class Mr
|
||||
---@field target? string
|
||||
---@field title? string
|
||||
---@field description? string
|
||||
|
||||
---@class Args
|
||||
---@field target? string
|
||||
---@field template_file? string
|
||||
|
||||
local M = {
|
||||
started = false,
|
||||
layout_visible = false,
|
||||
layout = nil,
|
||||
layout_buf = nil,
|
||||
title_bufnr = nil,
|
||||
description_bufnr = nil,
|
||||
mr = {
|
||||
target = "",
|
||||
title = "",
|
||||
description = "",
|
||||
},
|
||||
}
|
||||
|
||||
M.reset_state = function()
|
||||
M.started = false
|
||||
M.mr.title = ""
|
||||
M.mr.target = ""
|
||||
M.mr.description = ""
|
||||
end
|
||||
|
||||
local title_popup_settings = {
|
||||
buf_options = {
|
||||
filetype = "markdown",
|
||||
},
|
||||
focusable = true,
|
||||
border = {
|
||||
style = "rounded",
|
||||
text = {
|
||||
top = "Title",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local target_popup_settings = {
|
||||
buf_options = {
|
||||
filetype = "markdown",
|
||||
},
|
||||
focusable = true,
|
||||
border = {
|
||||
style = "rounded",
|
||||
text = {
|
||||
top = "Target branch",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local description_popup_settings = {
|
||||
buf_options = {
|
||||
filetype = "markdown",
|
||||
},
|
||||
enter = true,
|
||||
focusable = true,
|
||||
border = {
|
||||
style = "rounded",
|
||||
text = {
|
||||
top = "Description",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local title_input_options = {
|
||||
position = "50%",
|
||||
relative = "editor",
|
||||
size = 40,
|
||||
border = {
|
||||
style = "rounded",
|
||||
text = {
|
||||
top = "Title",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---1. If the user has already begun writing an MR, prompt them to
|
||||
--- continue working on it.
|
||||
---@param args? Args
|
||||
M.start = function(args)
|
||||
if M.started then
|
||||
vim.ui.select({ "Yes", "No" }, { prompt = "Continue your previous MR?" }, function(choice)
|
||||
if choice == "Yes" then
|
||||
M.open_confirmation_popup(M.mr)
|
||||
return
|
||||
else
|
||||
M.reset_state()
|
||||
M.pick_target(args)
|
||||
end
|
||||
end)
|
||||
else
|
||||
M.pick_target(args)
|
||||
end
|
||||
end
|
||||
|
||||
---2. Pick the target branch
|
||||
---@param args? Args
|
||||
M.pick_target = function(args)
|
||||
if not args then
|
||||
args = {}
|
||||
end
|
||||
if args.target ~= nil then
|
||||
M.pick_template({ target = args.target }, args)
|
||||
return
|
||||
end
|
||||
|
||||
if state.settings.create_mr.target ~= nil then
|
||||
M.pick_template({ target = state.settings.create_mr.target }, args)
|
||||
return
|
||||
end
|
||||
|
||||
local all_branch_names = u.get_all_git_branches(true)
|
||||
vim.ui.select(all_branch_names, {
|
||||
prompt = "Choose target branch for merge",
|
||||
}, function(choice)
|
||||
if choice then
|
||||
M.pick_template({ target = choice }, args)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function make_template_path(t)
|
||||
local base_dir = vim.fn.trim(vim.fn.system({ "git", "rev-parse", "--show-toplevel" }))
|
||||
return base_dir
|
||||
.. state.settings.file_separator
|
||||
.. ".gitlab"
|
||||
.. state.settings.file_separator
|
||||
.. "merge_request_templates"
|
||||
.. state.settings.file_separator
|
||||
.. t
|
||||
end
|
||||
|
||||
---3. Pick template (if applicable). This is used as the description
|
||||
---@param mr Mr
|
||||
---@param args Args
|
||||
M.pick_template = function(mr, args)
|
||||
if not args then
|
||||
args = {}
|
||||
end
|
||||
|
||||
local template_file = args.template_file or state.settings.create_mr.template_file
|
||||
if template_file ~= nil then
|
||||
local description = u.read_file(make_template_path(template_file))
|
||||
M.add_title({ target = mr.target, description = description })
|
||||
return
|
||||
end
|
||||
|
||||
local all_templates = u.list_files_in_folder(".gitlab" .. state.settings.file_separator .. "merge_request_templates")
|
||||
if all_templates == nil then
|
||||
M.add_title({ target = mr.target })
|
||||
return
|
||||
end
|
||||
|
||||
local opts = { "Blank Template" }
|
||||
for _, v in ipairs(all_templates) do
|
||||
table.insert(opts, v)
|
||||
end
|
||||
vim.ui.select(opts, {
|
||||
prompt = "Choose Template",
|
||||
}, function(choice)
|
||||
if choice then
|
||||
local description = u.read_file(make_template_path(choice))
|
||||
M.add_title({ target = mr.target, description = description })
|
||||
elseif choice == "Blank Template" then
|
||||
M.add_title({ target = mr.target })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---4. Prompts the user for the title of the MR
|
||||
---@param mr Mr
|
||||
M.add_title = function(mr)
|
||||
local input = Input(title_input_options, {
|
||||
prompt = "",
|
||||
default_value = "",
|
||||
on_close = function() end,
|
||||
on_submit = function(_value)
|
||||
M.open_confirmation_popup(mr)
|
||||
end,
|
||||
on_change = function(value)
|
||||
mr.title = value
|
||||
end,
|
||||
})
|
||||
input:map("n", "<Esc>", function()
|
||||
input:unmount()
|
||||
end, { noremap = true })
|
||||
|
||||
input:mount()
|
||||
end
|
||||
|
||||
---5. Show the final popup.
|
||||
---The function will render a popup containing the MR title and MR description, and
|
||||
---target branch. The title and description are editable.
|
||||
---@param mr Mr
|
||||
M.open_confirmation_popup = function(mr)
|
||||
M.started = true
|
||||
if M.layout_visible then
|
||||
M.layout:unmount()
|
||||
M.layout_visible = false
|
||||
return
|
||||
end
|
||||
|
||||
local layout, title_popup, description_popup, target_popup = M.create_layout()
|
||||
|
||||
M.layout = layout
|
||||
M.layout_buf = layout.bufnr
|
||||
M.layout_visible = true
|
||||
|
||||
local function exit()
|
||||
local title = vim.fn.trim(u.get_buffer_text(M.title_bufnr))
|
||||
local description = u.get_buffer_text(M.description_bufnr)
|
||||
local target = vim.fn.trim(u.get_buffer_text(target_popup.bufnr))
|
||||
M.mr = {
|
||||
title = title,
|
||||
description = description,
|
||||
target = target,
|
||||
}
|
||||
layout:unmount()
|
||||
M.layout_visible = false
|
||||
end
|
||||
|
||||
local description_lines = mr.description and M.build_description_lines(mr.description) or { "" }
|
||||
|
||||
vim.schedule(function()
|
||||
vim.api.nvim_buf_set_lines(description_popup.bufnr, 0, -1, false, description_lines)
|
||||
vim.api.nvim_buf_set_lines(title_popup.bufnr, 0, -1, false, { mr.title })
|
||||
vim.api.nvim_buf_set_lines(target_popup.bufnr, 0, -1, false, { mr.target })
|
||||
|
||||
local popup_opts = {
|
||||
cb = exit,
|
||||
action_before_close = true,
|
||||
action_before_exit = true,
|
||||
}
|
||||
|
||||
state.set_popup_keymaps(description_popup, M.create_mr, miscellaneous.attach_file, popup_opts)
|
||||
state.set_popup_keymaps(title_popup, M.create_mr, nil, popup_opts)
|
||||
state.set_popup_keymaps(target_popup, M.create_mr, nil, popup_opts)
|
||||
|
||||
vim.api.nvim_set_current_buf(description_popup.bufnr)
|
||||
end)
|
||||
end
|
||||
|
||||
---Builds a lua list of strings that contain the MR description
|
||||
M.build_description_lines = function(template_content)
|
||||
local description_lines = {}
|
||||
for line in template_content:gmatch("[^\n]+") do
|
||||
table.insert(description_lines, line)
|
||||
table.insert(description_lines, "")
|
||||
end
|
||||
|
||||
return description_lines
|
||||
end
|
||||
|
||||
---This function will POST the new MR to create it
|
||||
M.create_mr = function()
|
||||
local description = u.get_buffer_text(M.description_bufnr)
|
||||
local title = u.get_buffer_text(M.title_bufnr):gsub("\n", " ")
|
||||
local target = u.get_buffer_text(M.target_bufnr):gsub("\n", " ")
|
||||
|
||||
local body = {
|
||||
title = title,
|
||||
description = description,
|
||||
target_branch = target,
|
||||
}
|
||||
|
||||
job.run_job("/create_mr", "POST", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
M.reset_state()
|
||||
M.layout:unmount()
|
||||
M.layout_visible = false
|
||||
end)
|
||||
end
|
||||
|
||||
M.create_layout = function()
|
||||
local title_popup = Popup(title_popup_settings)
|
||||
M.title_bufnr = title_popup.bufnr
|
||||
local description_popup = Popup(description_popup_settings)
|
||||
M.description_bufnr = description_popup.bufnr
|
||||
local target_branch_popup = Popup(target_popup_settings)
|
||||
M.target_bufnr = target_branch_popup.bufnr
|
||||
|
||||
local internal_layout
|
||||
internal_layout = Layout.Box({
|
||||
Layout.Box({
|
||||
Layout.Box(title_popup, { grow = 1 }),
|
||||
Layout.Box(target_branch_popup, { grow = 1 }),
|
||||
}, { size = 3 }),
|
||||
Layout.Box(description_popup, { grow = 1 }),
|
||||
}, { dir = "col" })
|
||||
|
||||
local layout = Layout({
|
||||
position = "50%",
|
||||
relative = "editor",
|
||||
size = {
|
||||
width = "95%",
|
||||
height = "95%",
|
||||
},
|
||||
}, internal_layout)
|
||||
|
||||
layout:mount()
|
||||
|
||||
return layout, title_popup, description_popup, target_branch_popup
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -49,6 +49,7 @@
|
||||
---@field resolved_by Author
|
||||
---@field resolved_at string?
|
||||
---@field noteable_iid integer
|
||||
---@field url string?
|
||||
|
||||
---@class UnlinkedNote: Note
|
||||
---@field position nil
|
||||
|
||||
@@ -37,7 +37,7 @@ local M = {
|
||||
---callback with data
|
||||
---@param callback (fun(data: DiscussionData): nil)?
|
||||
M.load_discussions = function(callback)
|
||||
job.run_job("/discussions/list", "POST", { blacklist = state.settings.discussion_tree.blacklist }, function(data)
|
||||
job.run_job("/mr/discussions/list", "POST", { blacklist = state.settings.discussion_tree.blacklist }, function(data)
|
||||
M.discussions = data.discussions ~= vim.NIL and data.discussions or {}
|
||||
M.unlinked_discussions = data.unlinked_discussions ~= vim.NIL and data.unlinked_discussions or {}
|
||||
if type(callback) == "function" then
|
||||
@@ -52,7 +52,7 @@ M.initialize_discussions = function()
|
||||
-- Setup callback to refresh discussion data, discussion signs and diagnostics whenever the reviewed file changes.
|
||||
reviewer.set_callback_for_file_changed(M.refresh_discussion_data)
|
||||
-- Setup callback to clear signs and diagnostics whenever reviewer is left.
|
||||
reviewer.set_callback_for_reviewer_leave(signs.clear_signs_and_discussions)
|
||||
reviewer.set_callback_for_reviewer_leave(signs.clear_signs_and_diagnostics)
|
||||
end
|
||||
|
||||
---Refresh discussion data, signs, diagnostics, and winbar with new data from API
|
||||
@@ -209,7 +209,7 @@ end
|
||||
M.send_reply = function(tree, discussion_id)
|
||||
return function(text)
|
||||
local body = { discussion_id = discussion_id, reply = text }
|
||||
job.run_job("/reply", "POST", body, function(data)
|
||||
job.run_job("/mr/reply", "POST", body, function(data)
|
||||
u.notify("Sent reply!", vim.log.levels.INFO)
|
||||
M.add_reply_to_tree(tree, data.note, discussion_id)
|
||||
M.load_discussions()
|
||||
@@ -239,7 +239,7 @@ M.send_deletion = function(tree, unlinked)
|
||||
|
||||
local body = { discussion_id = root_node.id, note_id = tonumber(note_id) }
|
||||
|
||||
job.run_job("/comment", "DELETE", body, function(data)
|
||||
job.run_job("/mr/comment", "DELETE", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
if not note_node.is_root then
|
||||
tree:remove_node("-" .. note_id) -- Note is not a discussion root, safe to remove
|
||||
@@ -301,7 +301,7 @@ M.send_edits = function(discussion_id, note_id, unlinked)
|
||||
note_id = note_id,
|
||||
comment = text,
|
||||
}
|
||||
job.run_job("/comment", "PATCH", body, function(data)
|
||||
job.run_job("/mr/comment", "PATCH", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
M.rebuild_discussion_tree()
|
||||
if unlinked then
|
||||
@@ -327,7 +327,7 @@ M.toggle_discussion_resolved = function(tree)
|
||||
resolved = not note.resolved,
|
||||
}
|
||||
|
||||
job.run_job("/discussions/resolve", "PUT", body, function(data)
|
||||
job.run_job("/mr/discussions/resolve", "PUT", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
M.redraw_resolved_status(tree, note, not note.resolved)
|
||||
M.refresh_discussion_data()
|
||||
@@ -572,6 +572,12 @@ M.set_tree_keymaps = function(tree, bufnr, unlinked)
|
||||
end
|
||||
end, { buffer = bufnr, desc = "Jump to reviewer" })
|
||||
end
|
||||
vim.keymap.set("n", state.settings.discussion_tree.open_in_browser, function()
|
||||
M.open_in_browser(tree)
|
||||
end, { buffer = bufnr, desc = "Open the note in your browser" })
|
||||
vim.keymap.set("n", "<leader>p", function()
|
||||
M.print_node(tree)
|
||||
end, { buffer = bufnr, desc = "dev_ Print current node (for debugging)" })
|
||||
end
|
||||
|
||||
M.redraw_resolved_status = function(tree, note, mark_resolved)
|
||||
@@ -683,4 +689,26 @@ M.get_note_location = function(tree)
|
||||
nil
|
||||
end
|
||||
|
||||
---@param tree NuiTree
|
||||
M.open_in_browser = function(tree)
|
||||
local current_node = tree:get_node()
|
||||
local note_node = M.get_note_node(tree, current_node)
|
||||
if note_node == nil then
|
||||
return
|
||||
end
|
||||
local url = note_node.url
|
||||
if url == nil then
|
||||
u.notify("Could not get URL of note", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
u.open_in_browser(url)
|
||||
end
|
||||
|
||||
-- For developers!
|
||||
M.print_node = function(tree)
|
||||
local current_node = tree:get_node()
|
||||
vim.print(current_node)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -11,7 +11,7 @@ local M = {}
|
||||
M.diagnostics_namespace = diagnostics_namespace
|
||||
|
||||
---Clear all signs and diagnostics
|
||||
M.clear_signs_and_discussions = function()
|
||||
M.clear_signs_and_diagnostics = function()
|
||||
vim.fn.sign_unplace(discussion_sign_name)
|
||||
vim.diagnostic.reset(diagnostics_namespace)
|
||||
end
|
||||
|
||||
@@ -131,6 +131,7 @@ M.build_note = function(note, resolve_info)
|
||||
file_name = (type(note.position) == "table" and note.position.new_path),
|
||||
new_line = (type(note.position) == "table" and note.position.new_line),
|
||||
old_line = (type(note.position) == "table" and note.position.old_line),
|
||||
url = state.INFO.web_url .. "#note_" .. note.id,
|
||||
type = "note",
|
||||
}, text_nodes)
|
||||
|
||||
@@ -161,11 +162,11 @@ M.add_discussions_to_table = function(items, unlinked)
|
||||
local undefined_type = false
|
||||
local root_new_line = nil
|
||||
local root_old_line = nil
|
||||
local root_url
|
||||
|
||||
for j, note in ipairs(discussion.notes) do
|
||||
if j == 1 then
|
||||
_, root_text, root_text_nodes = M.build_note(note, { resolved = note.resolved, resolvable = note.resolvable })
|
||||
|
||||
root_file_name = (type(note.position) == "table" and note.position.new_path or nil)
|
||||
root_new_line = (type(note.position) == "table" and note.position.new_line or nil)
|
||||
root_old_line = (type(note.position) == "table" and note.position.old_line or nil)
|
||||
@@ -173,6 +174,7 @@ M.add_discussions_to_table = function(items, unlinked)
|
||||
root_note_id = tostring(note.id)
|
||||
resolvable = note.resolvable
|
||||
resolved = note.resolved
|
||||
root_url = state.INFO.web_url .. "#note_" .. note.id
|
||||
|
||||
-- This appears to be a Gitlab 🐛 where the "type" is returned as an empty string in some cases
|
||||
-- We link these comments to the old file by default
|
||||
@@ -203,6 +205,7 @@ M.add_discussions_to_table = function(items, unlinked)
|
||||
resolvable = resolvable,
|
||||
resolved = resolved,
|
||||
undefined_type = undefined_type,
|
||||
url = root_url,
|
||||
}, body)
|
||||
|
||||
table.insert(t, root_node)
|
||||
|
||||
@@ -52,7 +52,8 @@ end
|
||||
M.update_winbar = function(discussions, unlinked_discussions, base_title)
|
||||
local d = require("gitlab.actions.discussions")
|
||||
local winId = d.split.winid
|
||||
vim.wo[winId].winbar = content(discussions, unlinked_discussions, base_title)
|
||||
local c = content(discussions, unlinked_discussions, base_title)
|
||||
vim.wo[winId].winbar = c
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -46,7 +46,7 @@ M.confirm_merge = function(merge_body, squash_message)
|
||||
merge_body.squash_message = squash_message
|
||||
end
|
||||
|
||||
job.run_job("/merge", "POST", merge_body, function(data)
|
||||
job.run_job("/mr/merge", "POST", merge_body, function(data)
|
||||
reviewer.close()
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
end)
|
||||
|
||||
@@ -3,21 +3,6 @@ local u = require("gitlab.utils")
|
||||
local job = require("gitlab.job")
|
||||
local M = {}
|
||||
|
||||
M.open_in_browser = function()
|
||||
local url = state.INFO.web_url
|
||||
if url == nil then
|
||||
u.notify("Could not get Gitlab URL", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if vim.fn.has("mac") == 1 then
|
||||
vim.fn.jobstart({ "open", url })
|
||||
elseif vim.fn.has("unix") == 1 then
|
||||
vim.fn.jobstart({ "xdg-open", url })
|
||||
else
|
||||
u.notify("Opening a Gitlab URL is not supported on this OS!", vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
M.attach_file = function()
|
||||
local attachment_dir = state.settings.attachment_dir
|
||||
if not attachment_dir or attachment_dir == "" then
|
||||
@@ -40,7 +25,7 @@ M.attach_file = function()
|
||||
end
|
||||
local full_path = attachment_dir .. u.path_separator .. choice
|
||||
local body = { file_path = full_path, file_name = choice }
|
||||
job.run_job("/mr/attachment", "POST", body, function(data)
|
||||
job.run_job("/attachment", "POST", body, function(data)
|
||||
local markdown = data.markdown
|
||||
local current_line = u.get_current_line_number()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
@@ -9,8 +9,8 @@ local summary = require("gitlab.actions.summary")
|
||||
local assignees_and_reviewers = require("gitlab.actions.assignees_and_reviewers")
|
||||
local comment = require("gitlab.actions.comment")
|
||||
local pipeline = require("gitlab.actions.pipeline")
|
||||
local create_mr = require("gitlab.actions.create_mr")
|
||||
local approvals = require("gitlab.actions.approvals")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
|
||||
local info = state.dependencies.info
|
||||
local project_members = state.dependencies.project_members
|
||||
@@ -40,6 +40,7 @@ return {
|
||||
create_comment_suggestion = async.sequence({ info, revisions }, comment.create_comment_suggestion),
|
||||
move_to_discussion_tree_from_diagnostic = async.sequence({}, discussions.move_to_discussion_tree),
|
||||
create_note = async.sequence({ info }, comment.create_note),
|
||||
create_mr = async.sequence({}, create_mr.start),
|
||||
review = async.sequence({ u.merge(info, { refresh = true }), revisions }, function()
|
||||
reviewer.open()
|
||||
end),
|
||||
@@ -57,5 +58,11 @@ return {
|
||||
-- Other functions 🤷
|
||||
state = state,
|
||||
print_settings = state.print_settings,
|
||||
open_in_browser = async.sequence({ info }, miscellaneous.open_in_browser),
|
||||
open_in_browser = async.sequence({ info }, function()
|
||||
if state.INFO.web_url == nil then
|
||||
u.notify("Could not get Gitlab URL", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
u.open_in_browser(state.INFO.web_url)
|
||||
end),
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ M.settings = {
|
||||
jump_to_reviewer = "m",
|
||||
edit_comment = "e",
|
||||
delete_comment = "dd",
|
||||
open_in_browser = "b",
|
||||
reply = "r",
|
||||
toggle_node = "t",
|
||||
toggle_resolved = "p",
|
||||
@@ -71,6 +72,10 @@ M.settings = {
|
||||
squash = false,
|
||||
delete_branch = false,
|
||||
},
|
||||
create_mr = {
|
||||
target = nil,
|
||||
template_file = nil,
|
||||
},
|
||||
info = {
|
||||
enabled = true,
|
||||
horizontal = false,
|
||||
@@ -123,8 +128,8 @@ M.settings = {
|
||||
preparing = "",
|
||||
scheduled = "",
|
||||
running = "",
|
||||
canceled = "",
|
||||
skipped = "",
|
||||
canceled = "↪",
|
||||
skipped = "↪",
|
||||
success = "✓",
|
||||
failed = "",
|
||||
},
|
||||
@@ -204,7 +209,7 @@ M.setPluginConfiguration = function()
|
||||
end
|
||||
|
||||
local config_file_path = base_path .. M.settings.file_separator .. ".gitlab.nvim"
|
||||
local config_file_content = u.read_file(config_file_path)
|
||||
local config_file_content = u.read_file(config_file_path, { remove_newlines = true })
|
||||
|
||||
local file_properties = {}
|
||||
if config_file_content ~= nil then
|
||||
@@ -231,10 +236,15 @@ M.setPluginConfiguration = function()
|
||||
return true
|
||||
end
|
||||
|
||||
local function exit(popup, cb)
|
||||
local function exit(popup, opts)
|
||||
if opts.action_before_exit and opts.cb ~= nil then
|
||||
opts.cb()
|
||||
popup:unmount()
|
||||
if cb ~= nil then
|
||||
cb()
|
||||
else
|
||||
popup:unmount()
|
||||
if opts.cb ~= nil then
|
||||
opts.cb()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -244,7 +254,7 @@ M.set_popup_keymaps = function(popup, action, linewise_action, opts)
|
||||
opts = {}
|
||||
end
|
||||
vim.keymap.set("n", M.settings.popup.exit, function()
|
||||
exit(popup, opts.cb)
|
||||
exit(popup, opts)
|
||||
end, { buffer = popup.bufnr, desc = "Exit popup" })
|
||||
|
||||
if action ~= "Help" then -- Don't show help on the help popup
|
||||
@@ -258,9 +268,9 @@ M.set_popup_keymaps = function(popup, action, linewise_action, opts)
|
||||
local text = u.get_buffer_text(popup.bufnr)
|
||||
if opts.action_before_close then
|
||||
action(text, popup.bufnr)
|
||||
exit(popup)
|
||||
exit(popup, opts)
|
||||
else
|
||||
exit(popup)
|
||||
exit(popup, opts)
|
||||
action(text, popup.bufnr)
|
||||
end
|
||||
end, { buffer = popup.bufnr, desc = "Perform action" })
|
||||
@@ -282,7 +292,7 @@ end
|
||||
-- for each of the actions to occur. This is necessary because some Gitlab behaviors (like
|
||||
-- adding a reviewer) requires some initial state.
|
||||
M.dependencies = {
|
||||
info = { endpoint = "/info", key = "info", state = "INFO", refresh = false },
|
||||
info = { endpoint = "/mr/info", key = "info", state = "INFO", refresh = false },
|
||||
revisions = { endpoint = "/mr/revisions", key = "Revisions", state = "MR_REVISIONS", refresh = false },
|
||||
project_members = {
|
||||
endpoint = "/project/members",
|
||||
|
||||
@@ -382,14 +382,18 @@ M.create_popup_state = function(title, settings, width, height, zindex)
|
||||
return view_opts
|
||||
end
|
||||
|
||||
M.read_file = function(file_path)
|
||||
M.read_file = function(file_path, opts)
|
||||
local file = io.open(file_path, "r")
|
||||
if file == nil then
|
||||
return nil
|
||||
end
|
||||
local file_contents = file:read("*all")
|
||||
file:close()
|
||||
|
||||
if opts and opts.remove_newlines then
|
||||
file_contents = string.gsub(file_contents, "\n", "")
|
||||
end
|
||||
|
||||
return file_contents
|
||||
end
|
||||
|
||||
@@ -633,9 +637,46 @@ M.get_icon = function(filename)
|
||||
end
|
||||
end
|
||||
|
||||
---@param remote? boolean
|
||||
M.get_all_git_branches = function(remote)
|
||||
local branches = {}
|
||||
|
||||
local handle = remote == true and io.popen("git branch -r 2>&1") or io.popen("git branch 2>&1")
|
||||
|
||||
if handle then
|
||||
for line in handle:lines() do
|
||||
local branch
|
||||
if remote then
|
||||
for res in line:gmatch("origin/([^\n]+)") do
|
||||
branch = res -- Trim /origin
|
||||
end
|
||||
else
|
||||
branch = line:gsub("^%s*%*?%s*", "") -- Trim leading whitespace and the "* " marker for the current branch
|
||||
end
|
||||
table.insert(branches, branch)
|
||||
end
|
||||
handle:close()
|
||||
else
|
||||
print("Error running 'git branch' command.")
|
||||
end
|
||||
|
||||
return branches
|
||||
end
|
||||
|
||||
M.basename = function(str)
|
||||
local name = string.gsub(str, "(.*/)(.*)", "%2")
|
||||
return name
|
||||
end
|
||||
|
||||
---@param url string?
|
||||
M.open_in_browser = function(url)
|
||||
if vim.fn.has("mac") == 1 then
|
||||
vim.fn.jobstart({ "open", url })
|
||||
elseif vim.fn.has("unix") == 1 then
|
||||
vim.fn.jobstart({ "xdg-open", url })
|
||||
else
|
||||
M.notify("Opening a Gitlab URL is not supported on this OS!", vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -56,8 +56,12 @@ describe("gitlab/actions/discussions/tree.lua", function()
|
||||
|
||||
after_each(function()
|
||||
utils.time_since = original_time_since
|
||||
state.INFO = nil
|
||||
end)
|
||||
before_each(function()
|
||||
state.INFO = {
|
||||
web_url = "https://gitlab.com/some-org/-/merge_requests/4963",
|
||||
}
|
||||
spy_time_since = spy.new(function()
|
||||
return "5 days ago"
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user