Release 2.5.1 (#271)
* feat: Support for custom authentication provider functions (#270) * feat: Support for adding "draft" notes to the review, and publishing them, either individually or all at once. Addresses feature request #223. * feat: Lets users select + checkout a merge request directly within Neovim, without exiting to the terminal * fix: Checks that the remote feature branch exists and is up-to-date before creating a MR, starting a review, or opening the MR summary (#278) * docs: We require some state from Diffview, this shows how to load that state prior to installing w/ Packer. Fixes #94. This is a #MINOR release. --------- Co-authored-by: Jakub F. Bortlík <jakub.bortlik@proton.me> Co-authored-by: sunfuze <sunfuze.1989@gmail.com> Co-authored-by: Patrick Pichler <mail@patrickpichler.dev>
This commit is contained in:
committed by
GitHub
parent
f10c4ebb8f
commit
cf6ccddce3
65
README.md
65
README.md
@@ -25,9 +25,9 @@ To view these help docs and to get more detailed help information, please run `:
|
|||||||
|
|
||||||
1. Install Go
|
1. Install Go
|
||||||
2. Add configuration (see Installation section)
|
2. Add configuration (see Installation section)
|
||||||
3. Checkout your feature branch: `git checkout feature-branch`
|
5. Run `:lua require("gitlab").choose_merge_request()`
|
||||||
4. Open Neovim
|
|
||||||
5. Run `:lua require("gitlab").review()` to open the reviewer pane
|
This will checkout the branch locally, and open the plugin's reviewer pane.
|
||||||
|
|
||||||
For more detailed information about the Lua APIs please run `:h gitlab.nvim.api`
|
For more detailed information about the Lua APIs please run `:h gitlab.nvim.api`
|
||||||
|
|
||||||
@@ -56,20 +56,25 @@ return {
|
|||||||
And with Packer:
|
And with Packer:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
use {
|
use {
|
||||||
'harrisoncramer/gitlab.nvim',
|
"harrisoncramer/gitlab.nvim",
|
||||||
requires = {
|
requires = {
|
||||||
"MunifTanjim/nui.nvim",
|
"MunifTanjim/nui.nvim",
|
||||||
"nvim-lua/plenary.nvim",
|
"nvim-lua/plenary.nvim",
|
||||||
"sindrets/diffview.nvim",
|
"sindrets/diffview.nvim"
|
||||||
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
||||||
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
|
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
|
||||||
},
|
},
|
||||||
run = function() require("gitlab.server").build(true) end,
|
build = function()
|
||||||
config = function()
|
require("gitlab.server").build()
|
||||||
require("gitlab").setup()
|
end,
|
||||||
end,
|
branch = "develop",
|
||||||
}
|
config = function()
|
||||||
|
require("diffview") -- We require some global state from diffview
|
||||||
|
local gitlab = require("gitlab")
|
||||||
|
gitlab.setup()
|
||||||
|
end,
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Connecting to Gitlab
|
## Connecting to Gitlab
|
||||||
@@ -92,6 +97,18 @@ gitlab_url=https://my-personal-gitlab-instance.com/
|
|||||||
|
|
||||||
The plugin will look for the `.gitlab.nvim` file in the root of the current project by default. However, you may provide a custom path to the configuration file via the `config_path` option. This must be an absolute path to the directory that holds your `.gitlab.nvim` file.
|
The plugin will look for the `.gitlab.nvim` file in the root of the current project by default. However, you may provide a custom path to the configuration file via the `config_path` option. This must be an absolute path to the directory that holds your `.gitlab.nvim` file.
|
||||||
|
|
||||||
|
In case even more control over the auth config is needed, there is the possibility to override the `auth_provider` settings field. It should be
|
||||||
|
a function that returns the `token` as well as the `gitlab_url` value, and a nilable error. If the `gitlab_url` is `nil`, `https://gitlab.com` is used as default.
|
||||||
|
|
||||||
|
Here an example how to use a custom `auth_provider`:
|
||||||
|
```lua
|
||||||
|
require("gitlab").setup({
|
||||||
|
auth_provider = function()
|
||||||
|
return "my_token", "https://custom.gitlab.instance.url", nil
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
For more settings, please see `:h gitlab.nvim.connecting-to-gitlab`
|
For more settings, please see `:h gitlab.nvim.connecting-to-gitlab`
|
||||||
|
|
||||||
## Configuring the Plugin
|
## Configuring the Plugin
|
||||||
@@ -103,7 +120,10 @@ require("gitlab").setup({
|
|||||||
port = nil, -- The port of the Go server, which runs in the background, if omitted or `nil` the port will be chosen automatically
|
port = nil, -- The port of the Go server, which runs in the background, if omitted or `nil` the port will be chosen automatically
|
||||||
log_path = vim.fn.stdpath("cache") .. "/gitlab.nvim.log", -- Log path for the Go server
|
log_path = vim.fn.stdpath("cache") .. "/gitlab.nvim.log", -- Log path for the Go server
|
||||||
config_path = nil, -- Custom path for `.gitlab.nvim` file, please read the "Connecting to Gitlab" section
|
config_path = nil, -- Custom path for `.gitlab.nvim` file, please read the "Connecting to Gitlab" section
|
||||||
debug = { go_request = false, go_response = false }, -- Which values to log
|
debug = {
|
||||||
|
go_request = false,
|
||||||
|
go_response = false,
|
||||||
|
},
|
||||||
attachment_dir = nil, -- The local directory for files (see the "summary" section)
|
attachment_dir = nil, -- The local directory for files (see the "summary" section)
|
||||||
reviewer_settings = {
|
reviewer_settings = {
|
||||||
diffview = {
|
diffview = {
|
||||||
@@ -150,6 +170,7 @@ require("gitlab").setup({
|
|||||||
toggle_resolved_discussions = "R", -- Open or close all resolved discussions
|
toggle_resolved_discussions = "R", -- Open or close all resolved discussions
|
||||||
toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions
|
toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions
|
||||||
keep_current_open = false, -- If true, current discussion stays open even if it should otherwise be closed when toggling
|
keep_current_open = false, -- If true, current discussion stays open even if it should otherwise be closed when toggling
|
||||||
|
publish_draft = "P", -- Publishes the currently focused note/comment
|
||||||
toggle_resolved = "p" -- Toggles the resolved status of the whole discussion
|
toggle_resolved = "p" -- Toggles the resolved status of the whole discussion
|
||||||
position = "left", -- "top", "right", "bottom" or "left"
|
position = "left", -- "top", "right", "bottom" or "left"
|
||||||
open_in_browser = "b" -- Jump to the URL of the current note/discussion
|
open_in_browser = "b" -- Jump to the URL of the current note/discussion
|
||||||
@@ -160,9 +181,14 @@ require("gitlab").setup({
|
|||||||
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
|
tree_type = "simple", -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
|
||||||
toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
|
toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
|
||||||
|
draft_mode = false, -- Whether comments are posted as drafts as part of a review
|
||||||
|
toggle_draft_mode = "D" -- Toggle between draft mode (comments posted as drafts) and live mode (comments are posted immediately)
|
||||||
winbar = nil -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
|
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.
|
-- If using lualine, please add "gitlab" to disabled file types, otherwise you will not see the winbar.
|
||||||
},
|
},
|
||||||
|
choose_merge_request = {
|
||||||
|
open_reviewer = true, -- Open the reviewer window automatically after switching merge requests
|
||||||
|
},
|
||||||
info = { -- Show additional fields in the summary view
|
info = { -- Show additional fields in the summary view
|
||||||
enabled = true,
|
enabled = true,
|
||||||
horizontal = false, -- Display metadata to the left of the summary rather than underneath
|
horizontal = false, -- Display metadata to the left of the summary rather than underneath
|
||||||
@@ -246,6 +272,7 @@ you need to set them up yourself. Here's what I'm using:
|
|||||||
```lua
|
```lua
|
||||||
local gitlab = require("gitlab")
|
local gitlab = require("gitlab")
|
||||||
local gitlab_server = require("gitlab.server")
|
local gitlab_server = require("gitlab.server")
|
||||||
|
vim.keymap.set("n", "glb", gitlab.choose_merge_request)
|
||||||
vim.keymap.set("n", "glr", gitlab.review)
|
vim.keymap.set("n", "glr", gitlab.review)
|
||||||
vim.keymap.set("n", "gls", gitlab.summary)
|
vim.keymap.set("n", "gls", gitlab.summary)
|
||||||
vim.keymap.set("n", "glA", gitlab.approve)
|
vim.keymap.set("n", "glA", gitlab.approve)
|
||||||
@@ -266,6 +293,8 @@ vim.keymap.set("n", "glrd", gitlab.delete_reviewer)
|
|||||||
vim.keymap.set("n", "glp", gitlab.pipeline)
|
vim.keymap.set("n", "glp", gitlab.pipeline)
|
||||||
vim.keymap.set("n", "glo", gitlab.open_in_browser)
|
vim.keymap.set("n", "glo", gitlab.open_in_browser)
|
||||||
vim.keymap.set("n", "glM", gitlab.merge)
|
vim.keymap.set("n", "glM", gitlab.merge)
|
||||||
|
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
|
||||||
|
vim.keymap.set("n", "glP", gitlab.publish_all_drafts)
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information about each of these commands, and about the APIs in general, run `:h gitlab.nvim.api`
|
For more information about each of these commands, and about the APIs in general, run `:h gitlab.nvim.api`
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ syntax match ChevronDown ""
|
|||||||
syntax match ChevronRight ""
|
syntax match ChevronRight ""
|
||||||
syntax match Resolved /\s✓\s\?/
|
syntax match Resolved /\s✓\s\?/
|
||||||
syntax match Unresolved /\s-\s\?/
|
syntax match Unresolved /\s-\s\?/
|
||||||
|
syntax match Pencil //
|
||||||
|
|
||||||
highlight link Username GitlabUsername
|
highlight link Username GitlabUsername
|
||||||
highlight link Date GitlabDate
|
highlight link Date GitlabDate
|
||||||
@@ -15,5 +16,6 @@ highlight link ChevronDown GitlabChevron
|
|||||||
highlight link ChevronRight GitlabChevron
|
highlight link ChevronRight GitlabChevron
|
||||||
highlight link Resolved GitlabResolved
|
highlight link Resolved GitlabResolved
|
||||||
highlight link Unresolved GitlabUnresolved
|
highlight link Unresolved GitlabUnresolved
|
||||||
|
highlight link Pencil GitlabDraft
|
||||||
|
|
||||||
let b:current_syntax = "gitlab"
|
let b:current_syntax = "gitlab"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func updateAssigneesErr(pid interface{}, mergeRequest int, opt *gitlab.UpdateMer
|
|||||||
func TestAssigneeHandler(t *testing.T) {
|
func TestAssigneeHandler(t *testing.T) {
|
||||||
t.Run("Updates assignees", func(t *testing.T) {
|
t.Run("Updates assignees", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPut, "/mr/assignee", AssigneeUpdateRequest{Ids: []int{1, 2}})
|
request := makeRequest(t, http.MethodPut, "/mr/assignee", AssigneeUpdateRequest{Ids: []int{1, 2}})
|
||||||
server, _ := createRouterAndApi(fakeClient{updateMergeRequestFn: updateAssignees})
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequest: updateAssignees})
|
||||||
data := serveRequest(t, server, request, AssigneeUpdateResponse{})
|
data := serveRequest(t, server, request, AssigneeUpdateResponse{})
|
||||||
assert(t, data.SuccessResponse.Message, "Assignees updated")
|
assert(t, data.SuccessResponse.Message, "Assignees updated")
|
||||||
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
@@ -31,7 +31,7 @@ func TestAssigneeHandler(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Disallows non-PUT method", func(t *testing.T) {
|
t.Run("Disallows non-PUT method", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/assignee", nil)
|
request := makeRequest(t, http.MethodPost, "/mr/assignee", nil)
|
||||||
server, _ := createRouterAndApi(fakeClient{updateMergeRequestFn: updateAssignees})
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequest: updateAssignees})
|
||||||
data := serveRequest(t, server, request, ErrorResponse{})
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
assert(t, data.Status, http.StatusMethodNotAllowed)
|
assert(t, data.Status, http.StatusMethodNotAllowed)
|
||||||
assert(t, data.Details, "Invalid request type")
|
assert(t, data.Details, "Invalid request type")
|
||||||
@@ -40,7 +40,7 @@ func TestAssigneeHandler(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPut, "/mr/assignee", AssigneeUpdateRequest{Ids: []int{1, 2}})
|
request := makeRequest(t, http.MethodPut, "/mr/assignee", AssigneeUpdateRequest{Ids: []int{1, 2}})
|
||||||
server, _ := createRouterAndApi(fakeClient{updateMergeRequestFn: updateAssigneesErr})
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequest: updateAssigneesErr})
|
||||||
data := serveRequest(t, server, request, ErrorResponse{})
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
assert(t, data.Status, http.StatusInternalServerError)
|
assert(t, data.Status, http.StatusInternalServerError)
|
||||||
assert(t, data.Message, "Could not modify merge request assignees")
|
assert(t, data.Message, "Could not modify merge request assignees")
|
||||||
@@ -49,7 +49,7 @@ func TestAssigneeHandler(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPut, "/mr/assignee", AssigneeUpdateRequest{Ids: []int{1, 2}})
|
request := makeRequest(t, http.MethodPut, "/mr/assignee", AssigneeUpdateRequest{Ids: []int{1, 2}})
|
||||||
server, _ := createRouterAndApi(fakeClient{updateMergeRequestFn: updateAssigneesNon200})
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequest: updateAssigneesNon200})
|
||||||
data := serveRequest(t, server, request, ErrorResponse{})
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
assert(t, data.Status, http.StatusSeeOther)
|
assert(t, data.Status, http.StatusSeeOther)
|
||||||
assert(t, data.Message, "Could not modify merge request assignees")
|
assert(t, data.Message, "Could not modify merge request assignees")
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type Client struct {
|
|||||||
*gitlab.LabelsService
|
*gitlab.LabelsService
|
||||||
*gitlab.AwardEmojiService
|
*gitlab.AwardEmojiService
|
||||||
*gitlab.UsersService
|
*gitlab.UsersService
|
||||||
|
*gitlab.DraftNotesService
|
||||||
}
|
}
|
||||||
|
|
||||||
/* initGitlabClient parses and validates the project settings and initializes the Gitlab client. */
|
/* initGitlabClient parses and validates the project settings and initializes the Gitlab client. */
|
||||||
@@ -116,6 +117,7 @@ func initGitlabClient() (error, *Client) {
|
|||||||
LabelsService: client.Labels,
|
LabelsService: client.Labels,
|
||||||
AwardEmojiService: client.AwardEmoji,
|
AwardEmojiService: client.AwardEmoji,
|
||||||
UsersService: client.Users,
|
UsersService: client.Users,
|
||||||
|
DraftNotesService: client.DraftNotes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -11,28 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PostCommentRequest struct {
|
type PostCommentRequest struct {
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
FileName string `json:"file_name"`
|
PositionData
|
||||||
NewLine *int `json:"new_line,omitempty"`
|
|
||||||
OldLine *int `json:"old_line,omitempty"`
|
|
||||||
HeadCommitSHA string `json:"head_commit_sha"`
|
|
||||||
BaseCommitSHA string `json:"base_commit_sha"`
|
|
||||||
StartCommitSHA string `json:"start_commit_sha"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
LineRange *LineRange `json:"line_range,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LineRange represents the range of a note. */
|
|
||||||
type LineRange struct {
|
|
||||||
StartRange *LinePosition `json:"start"`
|
|
||||||
EndRange *LinePosition `json:"end"`
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LinePosition represents a position in a line range. Unlike the Gitlab struct, this does not contain LineCode with a sha1 of the filename */
|
|
||||||
type LinePosition struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
OldLine int `json:"old_line"`
|
|
||||||
NewLine int `json:"new_line"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteCommentRequest struct {
|
type DeleteCommentRequest struct {
|
||||||
@@ -53,6 +32,15 @@ type CommentResponse struct {
|
|||||||
Discussion *gitlab.Discussion `json:"discussion"`
|
Discussion *gitlab.Discussion `json:"discussion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CommentWithPosition is a comment with an (optional) position data value embedded in it. The position data will be non-nil for range-based comments. */
|
||||||
|
type CommentWithPosition struct {
|
||||||
|
PositionData PositionData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (comment CommentWithPosition) GetPositionData() PositionData {
|
||||||
|
return comment.PositionData
|
||||||
|
}
|
||||||
|
|
||||||
/* commentHandler creates, edits, and deletes discussions (comments, multi-line comments) */
|
/* commentHandler creates, edits, and deletes discussions (comments, multi-line comments) */
|
||||||
func (a *api) commentHandler(w http.ResponseWriter, r *http.Request) {
|
func (a *api) commentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -133,46 +121,10 @@ func (a *api) postComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
/* If we are leaving a comment on a line, leave position. Otherwise,
|
/* If we are leaving a comment on a line, leave position. Otherwise,
|
||||||
we are leaving a note (unlinked comment) */
|
we are leaving a note (unlinked comment) */
|
||||||
var friendlyName = "Note"
|
|
||||||
if postCommentRequest.FileName != "" {
|
|
||||||
friendlyName = "Comment"
|
|
||||||
opt.Position = &gitlab.PositionOptions{
|
|
||||||
PositionType: &postCommentRequest.Type,
|
|
||||||
StartSHA: &postCommentRequest.StartCommitSHA,
|
|
||||||
HeadSHA: &postCommentRequest.HeadCommitSHA,
|
|
||||||
BaseSHA: &postCommentRequest.BaseCommitSHA,
|
|
||||||
NewPath: &postCommentRequest.FileName,
|
|
||||||
OldPath: &postCommentRequest.FileName,
|
|
||||||
NewLine: postCommentRequest.NewLine,
|
|
||||||
OldLine: postCommentRequest.OldLine,
|
|
||||||
}
|
|
||||||
|
|
||||||
if postCommentRequest.LineRange != nil {
|
if postCommentRequest.FileName != "" {
|
||||||
friendlyName = "Multiline Comment"
|
commentWithPositionData := CommentWithPosition{postCommentRequest.PositionData}
|
||||||
shaFormat := "%x_%d_%d"
|
opt.Position = buildCommentPosition(commentWithPositionData)
|
||||||
startFilenameSha := fmt.Sprintf(
|
|
||||||
shaFormat,
|
|
||||||
sha1.Sum([]byte(postCommentRequest.FileName)),
|
|
||||||
postCommentRequest.LineRange.StartRange.OldLine,
|
|
||||||
postCommentRequest.LineRange.StartRange.NewLine,
|
|
||||||
)
|
|
||||||
endFilenameSha := fmt.Sprintf(
|
|
||||||
shaFormat,
|
|
||||||
sha1.Sum([]byte(postCommentRequest.FileName)),
|
|
||||||
postCommentRequest.LineRange.EndRange.OldLine,
|
|
||||||
postCommentRequest.LineRange.EndRange.NewLine,
|
|
||||||
)
|
|
||||||
opt.Position.LineRange = &gitlab.LineRangeOptions{
|
|
||||||
Start: &gitlab.LinePositionOptions{
|
|
||||||
Type: &postCommentRequest.LineRange.StartRange.Type,
|
|
||||||
LineCode: &startFilenameSha,
|
|
||||||
},
|
|
||||||
End: &gitlab.LinePositionOptions{
|
|
||||||
Type: &postCommentRequest.LineRange.EndRange.Type,
|
|
||||||
LineCode: &endFilenameSha,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
discussion, res, err := a.client.CreateMergeRequestDiscussion(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opt)
|
discussion, res, err := a.client.CreateMergeRequestDiscussion(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opt)
|
||||||
@@ -190,7 +142,7 @@ func (a *api) postComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := CommentResponse{
|
response := CommentResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Message: fmt.Sprintf("%s created successfully", friendlyName),
|
Message: "Comment created successfully",
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
},
|
},
|
||||||
Comment: discussion.Notes[0],
|
Comment: discussion.Notes[0],
|
||||||
|
|||||||
82
cmd/comment_helpers.go
Normal file
82
cmd/comment_helpers.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* LinePosition represents a position in a line range. Unlike the Gitlab struct, this does not contain LineCode with a sha1 of the filename */
|
||||||
|
type LinePosition struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
OldLine int `json:"old_line"`
|
||||||
|
NewLine int `json:"new_line"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LineRange represents the range of a note. */
|
||||||
|
type LineRange struct {
|
||||||
|
StartRange *LinePosition `json:"start"`
|
||||||
|
EndRange *LinePosition `json:"end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PositionData represents the position of a comment or note (relative to a file diff) */
|
||||||
|
type PositionData struct {
|
||||||
|
FileName string `json:"file_name"`
|
||||||
|
NewLine *int `json:"new_line,omitempty"`
|
||||||
|
OldLine *int `json:"old_line,omitempty"`
|
||||||
|
HeadCommitSHA string `json:"head_commit_sha"`
|
||||||
|
BaseCommitSHA string `json:"base_commit_sha"`
|
||||||
|
StartCommitSHA string `json:"start_commit_sha"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
LineRange *LineRange `json:"line_range,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RequestWithPosition is an interface that abstracts the handling of position data for a comment or a draft comment */
|
||||||
|
type RequestWithPosition interface {
|
||||||
|
GetPositionData() PositionData
|
||||||
|
}
|
||||||
|
|
||||||
|
/* buildCommentPosition takes a comment or draft comment request and builds the position data necessary for a location-based comment */
|
||||||
|
func buildCommentPosition(commentWithPositionData RequestWithPosition) *gitlab.PositionOptions {
|
||||||
|
positionData := commentWithPositionData.GetPositionData()
|
||||||
|
|
||||||
|
opt := &gitlab.PositionOptions{
|
||||||
|
PositionType: &positionData.Type,
|
||||||
|
StartSHA: &positionData.StartCommitSHA,
|
||||||
|
HeadSHA: &positionData.HeadCommitSHA,
|
||||||
|
BaseSHA: &positionData.BaseCommitSHA,
|
||||||
|
NewPath: &positionData.FileName,
|
||||||
|
OldPath: &positionData.FileName,
|
||||||
|
NewLine: positionData.NewLine,
|
||||||
|
OldLine: positionData.OldLine,
|
||||||
|
}
|
||||||
|
|
||||||
|
if positionData.LineRange != nil {
|
||||||
|
shaFormat := "%x_%d_%d"
|
||||||
|
startFilenameSha := fmt.Sprintf(
|
||||||
|
shaFormat,
|
||||||
|
sha1.Sum([]byte(positionData.FileName)),
|
||||||
|
positionData.LineRange.StartRange.OldLine,
|
||||||
|
positionData.LineRange.StartRange.NewLine,
|
||||||
|
)
|
||||||
|
endFilenameSha := fmt.Sprintf(
|
||||||
|
shaFormat,
|
||||||
|
sha1.Sum([]byte(positionData.FileName)),
|
||||||
|
positionData.LineRange.EndRange.OldLine,
|
||||||
|
positionData.LineRange.EndRange.NewLine,
|
||||||
|
)
|
||||||
|
opt.LineRange = &gitlab.LineRangeOptions{
|
||||||
|
Start: &gitlab.LinePositionOptions{
|
||||||
|
Type: &positionData.LineRange.StartRange.Type,
|
||||||
|
LineCode: &startFilenameSha,
|
||||||
|
},
|
||||||
|
End: &gitlab.LinePositionOptions{
|
||||||
|
Type: &positionData.LineRange.EndRange.Type,
|
||||||
|
LineCode: &endFilenameSha,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt
|
||||||
|
}
|
||||||
@@ -25,12 +25,16 @@ func TestPostComment(t *testing.T) {
|
|||||||
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{})
|
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{})
|
||||||
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
||||||
data := serveRequest(t, server, request, CommentResponse{})
|
data := serveRequest(t, server, request, CommentResponse{})
|
||||||
assert(t, data.SuccessResponse.Message, "Note created successfully")
|
assert(t, data.SuccessResponse.Message, "Comment created successfully")
|
||||||
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Creates a new comment", func(t *testing.T) {
|
t.Run("Creates a new comment", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{FileName: "some_file.txt"})
|
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{
|
||||||
|
PositionData: PositionData{
|
||||||
|
FileName: "some_file.txt",
|
||||||
|
},
|
||||||
|
})
|
||||||
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
||||||
data := serveRequest(t, server, request, CommentResponse{})
|
data := serveRequest(t, server, request, CommentResponse{})
|
||||||
assert(t, data.SuccessResponse.Message, "Comment created successfully")
|
assert(t, data.SuccessResponse.Message, "Comment created successfully")
|
||||||
@@ -39,15 +43,17 @@ func TestPostComment(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Creates a new multiline comment", func(t *testing.T) {
|
t.Run("Creates a new multiline comment", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{
|
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{
|
||||||
FileName: "some_file.txt",
|
PositionData: PositionData{
|
||||||
LineRange: &LineRange{
|
FileName: "some_file.txt",
|
||||||
StartRange: &LinePosition{}, /* These would have real data */
|
LineRange: &LineRange{
|
||||||
EndRange: &LinePosition{},
|
StartRange: &LinePosition{}, /* These would have real data */
|
||||||
|
EndRange: &LinePosition{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
||||||
data := serveRequest(t, server, request, CommentResponse{})
|
data := serveRequest(t, server, request, CommentResponse{})
|
||||||
assert(t, data.SuccessResponse.Message, "Multiline Comment created successfully")
|
assert(t, data.SuccessResponse.Message, "Comment created successfully")
|
||||||
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
306
cmd/draft_notes.go
Normal file
306
cmd/draft_notes.go
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* The data coming from the client when creating a draft note is the same,
|
||||||
|
as when they are creating a normal comment, but the Gitlab
|
||||||
|
endpoints + resources we handle are different */
|
||||||
|
|
||||||
|
type PostDraftNoteRequest struct {
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
PositionData
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateDraftNoteRequest struct {
|
||||||
|
Note string `json:"note"`
|
||||||
|
Position gitlab.PositionOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type DraftNotePublishRequest struct {
|
||||||
|
Note int `json:"note,omitempty"`
|
||||||
|
PublishAll bool `json:"publish_all"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DraftNoteResponse struct {
|
||||||
|
SuccessResponse
|
||||||
|
DraftNote *gitlab.DraftNote `json:"draft_note"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListDraftNotesResponse struct {
|
||||||
|
SuccessResponse
|
||||||
|
DraftNotes []*gitlab.DraftNote `json:"draft_notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DraftNoteWithPosition is a draft comment with an (optional) position data value embedded in it. The position data will be non-nil for range-based draft comments. */
|
||||||
|
type DraftNoteWithPosition struct {
|
||||||
|
PositionData PositionData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (draftNote DraftNoteWithPosition) GetPositionData() PositionData {
|
||||||
|
return draftNote.PositionData
|
||||||
|
}
|
||||||
|
|
||||||
|
/* draftNoteHandler creates, edits, and deletes draft notes */
|
||||||
|
func (a *api) draftNoteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
a.listDraftNotes(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
a.postDraftNote(w, r)
|
||||||
|
case http.MethodPatch:
|
||||||
|
a.updateDraftNote(w, r)
|
||||||
|
case http.MethodDelete:
|
||||||
|
a.deleteDraftNote(w, r)
|
||||||
|
default:
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, %s, %s, %s", http.MethodDelete, http.MethodPost, http.MethodPatch, http.MethodGet))
|
||||||
|
handleError(w, InvalidRequestError{}, "Expected DELETE, GET, POST or PATCH", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *api) draftNotePublisher(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", 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
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
var draftNotePublishRequest DraftNotePublishRequest
|
||||||
|
err = json.Unmarshal(body, &draftNotePublishRequest)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gitlab.Response
|
||||||
|
if draftNotePublishRequest.PublishAll {
|
||||||
|
res, err = a.client.PublishAllDraftNotes(a.projectInfo.ProjectId, a.projectInfo.MergeId)
|
||||||
|
} else {
|
||||||
|
if draftNotePublishRequest.Note == 0 {
|
||||||
|
handleError(w, errors.New("No ID provided"), "Must provide Note ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err = a.client.PublishDraftNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, draftNotePublishRequest.Note)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not publish draft note(s)", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/mr/draft_notes/publish"}, "Could not publish dfaft note", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := SuccessResponse{
|
||||||
|
Message: "Draft note(s) published",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(response)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* postDraftNote creates a draft note */
|
||||||
|
func (a *api) postDraftNote(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var postDraftNoteRequest PostDraftNoteRequest
|
||||||
|
err = json.Unmarshal(body, &postDraftNoteRequest)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitlab.CreateDraftNoteOptions{
|
||||||
|
Note: &postDraftNoteRequest.Comment,
|
||||||
|
// TODO: Support posting replies as drafts and rendering draft replies in the discussion tree
|
||||||
|
// instead of the notes tree
|
||||||
|
// InReplyToDiscussionID *string `url:"in_reply_to_discussion_id,omitempty" json:"in_reply_to_discussion_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if postDraftNoteRequest.FileName != "" {
|
||||||
|
draftNoteWithPosition := DraftNoteWithPosition{postDraftNoteRequest.PositionData}
|
||||||
|
opt.Position = buildCommentPosition(draftNoteWithPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
draftNote, res, err := a.client.CreateDraftNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not create draft note", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/mr/draft_notes/"}, "Could not create draft note", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := DraftNoteResponse{
|
||||||
|
SuccessResponse: SuccessResponse{
|
||||||
|
Message: "Draft note created successfully",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
},
|
||||||
|
DraftNote: draftNote,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(response)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* deleteDraftNote deletes a draft note */
|
||||||
|
func (a *api) deleteDraftNote(w http.ResponseWriter, r *http.Request) {
|
||||||
|
suffix := strings.TrimPrefix(r.URL.Path, "/mr/draft_notes/")
|
||||||
|
id, err := strconv.Atoi(suffix)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not parse draft note ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := a.client.DeleteDraftNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not delete draft note", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/mr/draft_notes/"}, "Could not delete draft note", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := SuccessResponse{
|
||||||
|
Message: "Draft note deleted",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(response)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* updateDraftNote edits the text of a draft comment */
|
||||||
|
func (a *api) updateDraftNote(w http.ResponseWriter, r *http.Request) {
|
||||||
|
suffix := strings.TrimPrefix(r.URL.Path, "/mr/draft_notes/")
|
||||||
|
id, err := strconv.Atoi(suffix)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not parse draft note ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var updateDraftNoteRequest UpdateDraftNoteRequest
|
||||||
|
err = json.Unmarshal(body, &updateDraftNoteRequest)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateDraftNoteRequest.Note == "" {
|
||||||
|
handleError(w, errors.New("Draft note text missing"), "Must provide draft note text", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitlab.UpdateDraftNoteOptions{
|
||||||
|
Note: &updateDraftNoteRequest.Note,
|
||||||
|
Position: &updateDraftNoteRequest.Position,
|
||||||
|
}
|
||||||
|
|
||||||
|
draftNote, res, err := a.client.UpdateDraftNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, id, &opt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not update draft note", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/mr/draft_notes/"}, "Could not update draft note", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := DraftNoteResponse{
|
||||||
|
SuccessResponse: SuccessResponse{
|
||||||
|
Message: "Draft note updated",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
},
|
||||||
|
DraftNote: draftNote,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(response)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* listDraftNotes lists all draft notes for the currently authenticated user */
|
||||||
|
func (a *api) listDraftNotes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
opt := gitlab.ListDraftNotesOptions{}
|
||||||
|
draftNotes, res, err := a.client.ListDraftNotes(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not get draft notes", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/mr/draft/comment"}, "Could not get draft notes", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := ListDraftNotesResponse{
|
||||||
|
SuccessResponse: SuccessResponse{
|
||||||
|
Message: "Draft notes fetched successfully",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
},
|
||||||
|
DraftNotes: draftNotes,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(response)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
197
cmd/draft_notes_test.go
Normal file
197
cmd/draft_notes_test.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listDraftNotes(pid interface{}, mergeRequest int, opt *gitlab.ListDraftNotesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.DraftNote, *gitlab.Response, error) {
|
||||||
|
return []*gitlab.DraftNote{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listDraftNotesErr(pid interface{}, mergeRequest int, opt *gitlab.ListDraftNotesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.DraftNote, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusInternalServerError), errors.New("Some error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListDraftNotes(t *testing.T) {
|
||||||
|
t.Run("Lists all draft notes", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listDraftNotes: listDraftNotes})
|
||||||
|
data := serveRequest(t, server, request, ListDraftNotesResponse{})
|
||||||
|
|
||||||
|
assert(t, data.SuccessResponse.Message, "Draft notes fetched successfully")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles error", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/mr/draft_notes/", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listDraftNotes: listDraftNotesErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
|
||||||
|
assert(t, data.Message, "Could not get draft notes")
|
||||||
|
assert(t, data.Status, http.StatusInternalServerError)
|
||||||
|
assert(t, data.Details, "Some error")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDraftNote(pid interface{}, mergeRequestIID int, opt *gitlab.CreateDraftNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.DraftNote, *gitlab.Response, error) {
|
||||||
|
return &gitlab.DraftNote{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDraftNoteErr(pid interface{}, mergeRequestIID int, opt *gitlab.CreateDraftNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.DraftNote, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusInternalServerError), errors.New("Some error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostDraftNote(t *testing.T) {
|
||||||
|
t.Run("Posts new draft note", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/", PostDraftNoteRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{createDraftNote: createDraftNote})
|
||||||
|
|
||||||
|
data := serveRequest(t, server, request, DraftNoteResponse{})
|
||||||
|
|
||||||
|
assert(t, data.SuccessResponse.Message, "Draft note created successfully")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors on draft note creation", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/", PostDraftNoteRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{createDraftNote: createDraftNoteErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "Could not create draft note")
|
||||||
|
assert(t, data.Status, http.StatusInternalServerError)
|
||||||
|
assert(t, data.Details, "Some error")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteDraftNote(pid interface{}, mergeRequest int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteDraftNoteErr(pid interface{}, mergeRequest int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return makeResponse(http.StatusInternalServerError), errors.New("Something went wrong")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteDraftNote(t *testing.T) {
|
||||||
|
t.Run("Deletes draft note", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodDelete, "/mr/draft_notes/3", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{deleteDraftNote: deleteDraftNote})
|
||||||
|
data := serveRequest(t, server, request, SuccessResponse{})
|
||||||
|
assert(t, data.Message, "Draft note deleted")
|
||||||
|
assert(t, data.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles error", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodDelete, "/mr/draft_notes/3", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{deleteDraftNote: deleteDraftNoteErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "Could not delete draft note")
|
||||||
|
assert(t, data.Status, http.StatusInternalServerError)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles bad ID", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodDelete, "/mr/draft_notes/abc", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{deleteDraftNote: deleteDraftNote})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "Could not parse draft note ID")
|
||||||
|
assert(t, data.Status, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDraftNote(pid interface{}, mergeRequest int, note int, opt *gitlab.UpdateDraftNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.DraftNote, *gitlab.Response, error) {
|
||||||
|
return &gitlab.DraftNote{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditDraftNote(t *testing.T) {
|
||||||
|
t.Run("Edits draft note", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/3", UpdateDraftNoteRequest{Note: "Some new note", Position: gitlab.PositionOptions{}})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateDraftNote: updateDraftNote})
|
||||||
|
data := serveRequest(t, server, request, SuccessResponse{})
|
||||||
|
assert(t, data.Message, "Draft note updated")
|
||||||
|
assert(t, data.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles bad ID", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/abc", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateDraftNote: updateDraftNote})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "Could not parse draft note ID")
|
||||||
|
assert(t, data.Status, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles empty note", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPatch, "/mr/draft_notes/3", UpdateDraftNoteRequest{Note: ""})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateDraftNote: updateDraftNote})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "Must provide draft note text")
|
||||||
|
assert(t, data.Status, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishDraftNote(pid interface{}, mergeRequest int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishDraftNoteErr(pid interface{}, mergeRequest int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return nil, errors.New("Some error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishDraftNote(t *testing.T) {
|
||||||
|
t.Run("Should publish a draft note", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", DraftNotePublishRequest{Note: 3, PublishAll: false})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{
|
||||||
|
publishDraftNote: publishDraftNote,
|
||||||
|
})
|
||||||
|
data := serveRequest(t, server, request, SuccessResponse{})
|
||||||
|
assert(t, data.Message, "Draft note(s) published")
|
||||||
|
assert(t, data.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles bad/missing ID", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", DraftNotePublishRequest{PublishAll: false})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{publishDraftNote: publishDraftNote})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "Must provide Note ID")
|
||||||
|
assert(t, data.Status, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles error", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", DraftNotePublishRequest{PublishAll: false, Note: 3})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{publishDraftNote: publishDraftNoteErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "Could not publish draft note(s)")
|
||||||
|
assert(t, data.Status, http.StatusInternalServerError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishAllDraftNotes(pid interface{}, mergeRequest int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishAllDraftNotesErr(pid interface{}, mergeRequest int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return nil, errors.New("Some error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishAllDraftNotes(t *testing.T) {
|
||||||
|
t.Run("Should publish all draft notes", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", DraftNotePublishRequest{PublishAll: true})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{
|
||||||
|
publishAllDraftNotes: publishAllDraftNotes,
|
||||||
|
})
|
||||||
|
data := serveRequest(t, server, request, SuccessResponse{})
|
||||||
|
assert(t, data.Message, "Draft note(s) published")
|
||||||
|
assert(t, data.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should handle an error", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/draft_notes/publish", DraftNotePublishRequest{PublishAll: true})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{
|
||||||
|
publishAllDraftNotes: publishAllDraftNotesErr,
|
||||||
|
})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "Could not publish draft note(s)")
|
||||||
|
assert(t, data.Status, http.StatusInternalServerError)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ func getInfoErr(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsO
|
|||||||
func TestInfoHandler(t *testing.T) {
|
func TestInfoHandler(t *testing.T) {
|
||||||
t.Run("Returns normal information", func(t *testing.T) {
|
t.Run("Returns normal information", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
||||||
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfo})
|
server, _ := createRouterAndApi(fakeClient{getMergeRequest: getInfo})
|
||||||
data := serveRequest(t, server, request, InfoResponse{})
|
data := serveRequest(t, server, request, InfoResponse{})
|
||||||
assert(t, data.Info.Title, "Some Title")
|
assert(t, data.Info.Title, "Some Title")
|
||||||
assert(t, data.SuccessResponse.Message, "Merge requests retrieved")
|
assert(t, data.SuccessResponse.Message, "Merge requests retrieved")
|
||||||
@@ -32,21 +32,21 @@ func TestInfoHandler(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Disallows non-GET method", func(t *testing.T) {
|
t.Run("Disallows non-GET method", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/info", nil)
|
request := makeRequest(t, http.MethodPost, "/mr/info", nil)
|
||||||
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfo})
|
server, _ := createRouterAndApi(fakeClient{getMergeRequest: getInfo})
|
||||||
data := serveRequest(t, server, request, ErrorResponse{})
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
checkBadMethod(t, *data, http.MethodGet)
|
checkBadMethod(t, *data, http.MethodGet)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
||||||
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfoErr})
|
server, _ := createRouterAndApi(fakeClient{getMergeRequest: getInfoErr})
|
||||||
data := serveRequest(t, server, request, ErrorResponse{})
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
checkErrorFromGitlab(t, *data, "Could not get project info")
|
checkErrorFromGitlab(t, *data, "Could not get project info")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
|
||||||
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfoNon200})
|
server, _ := createRouterAndApi(fakeClient{getMergeRequest: getInfoNon200})
|
||||||
data := serveRequest(t, server, request, ErrorResponse{})
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
checkNon200(t, *data, "Could not get project info", "/mr/info")
|
checkNon200(t, *data, "Could not get project info", "/mr/info")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
gitInfo, err := extractGitInfo(RefreshProjectInfo, GetProjectUrlFromNativeGitCmd, GetCurrentBranchNameFromNativeGitCmd)
|
gitInfo, err := extractGitInfo(RefreshProjectInfo, GetProjectUrlFromNativeGitCmd, GetCurrentBranchNameFromNativeGitCmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failure initializing plugin with `git` commands: %v", err)
|
log.Fatalf("Failure initializing plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err, client := initGitlabClient()
|
err, client := initGitlabClient()
|
||||||
|
|||||||
55
cmd/merge_requests.go
Normal file
55
cmd/merge_requests.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListMergeRequestResponse struct {
|
||||||
|
SuccessResponse
|
||||||
|
MergeRequests []*gitlab.MergeRequest `json:"merge_requests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *api) mergeRequestsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
||||||
|
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := gitlab.ListProjectMergeRequestsOptions{
|
||||||
|
Scope: gitlab.Ptr("all"),
|
||||||
|
State: gitlab.Ptr("opened"),
|
||||||
|
}
|
||||||
|
|
||||||
|
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, errors.New("No merge requests found"), "No merge requests found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := ListMergeRequestResponse{
|
||||||
|
SuccessResponse: SuccessResponse{
|
||||||
|
Message: "Merge requests fetched successfully",
|
||||||
|
Status: http.StatusOK,
|
||||||
|
},
|
||||||
|
MergeRequests: mergeRequests,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(response)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
45
cmd/merge_requests_test.go
Normal file
45
cmd/merge_requests_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listProjectMergeRequests200(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return []*gitlab.MergeRequest{{ID: 1}}, &gitlab.Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listProjectMergeRequestsEmpty(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return []*gitlab.MergeRequest{}, &gitlab.Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listProjectMergeRequestsErr(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeRequestHandler(t *testing.T) {
|
||||||
|
t.Run("Should fetch merge requests", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/merge_requests", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listProjectMergeRequests: listProjectMergeRequests200})
|
||||||
|
data := serveRequest(t, server, request, ListMergeRequestResponse{})
|
||||||
|
assert(t, data.Message, "Merge requests fetched successfully")
|
||||||
|
assert(t, data.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
t.Run("Should handle an error", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/merge_requests", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listProjectMergeRequests: listProjectMergeRequestsErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "Failed to list merge requests")
|
||||||
|
assert(t, data.Status, http.StatusInternalServerError)
|
||||||
|
})
|
||||||
|
t.Run("Should handle not having any merge requests with 404", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/merge_requests", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listProjectMergeRequests: listProjectMergeRequestsEmpty})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Message, "No merge requests found")
|
||||||
|
assert(t, data.Status, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
func acceptAndMergeFn(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
func acceptMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
return &gitlab.MergeRequest{}, makeResponse(http.StatusOK), nil
|
return &gitlab.MergeRequest{}, makeResponse(http.StatusOK), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptAndMergeFnErr(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
func acceptMergeRequestErr(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
return nil, nil, errors.New("Some error from Gitlab")
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ func acceptAndMergeNon200(pid interface{}, mergeRequest int, opt *gitlab.AcceptM
|
|||||||
func TestAcceptAndMergeHandler(t *testing.T) {
|
func TestAcceptAndMergeHandler(t *testing.T) {
|
||||||
t.Run("Accepts and merges a merge request", func(t *testing.T) {
|
t.Run("Accepts and merges a merge request", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
|
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
|
||||||
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFn})
|
server, _ := createRouterAndApi(fakeClient{acceptMergeRequest: acceptMergeRequest})
|
||||||
data := serveRequest(t, server, request, SuccessResponse{})
|
data := serveRequest(t, server, request, SuccessResponse{})
|
||||||
assert(t, data.Message, "MR merged successfully")
|
assert(t, data.Message, "MR merged successfully")
|
||||||
assert(t, data.Status, http.StatusOK)
|
assert(t, data.Status, http.StatusOK)
|
||||||
@@ -31,21 +31,21 @@ func TestAcceptAndMergeHandler(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Disallows non-POST methods", func(t *testing.T) {
|
t.Run("Disallows non-POST methods", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodGet, "/mr/merge", AcceptMergeRequestRequest{})
|
request := makeRequest(t, http.MethodGet, "/mr/merge", AcceptMergeRequestRequest{})
|
||||||
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFn})
|
server, _ := createRouterAndApi(fakeClient{acceptMergeRequest: acceptMergeRequest})
|
||||||
data := serveRequest(t, server, request, ErrorResponse{})
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
checkBadMethod(t, *data, http.MethodPost)
|
checkBadMethod(t, *data, http.MethodPost)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
|
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
|
||||||
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFnErr})
|
server, _ := createRouterAndApi(fakeClient{acceptMergeRequest: acceptMergeRequestErr})
|
||||||
data := serveRequest(t, server, request, ErrorResponse{})
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
checkErrorFromGitlab(t, *data, "Could not merge MR")
|
checkErrorFromGitlab(t, *data, "Could not merge MR")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
|
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
|
||||||
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeNon200})
|
server, _ := createRouterAndApi(fakeClient{acceptMergeRequest: acceptAndMergeNon200})
|
||||||
data := serveRequest(t, server, request, ErrorResponse{})
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
checkNon200(t, *data, "Could not merge MR", "/mr/merge")
|
checkNon200(t, *data, "Could not merge MR", "/mr/merge")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ func createRouterAndApi(client ClientInterface, optFuncs ...optFunc) (*http.Serv
|
|||||||
m.HandleFunc("/mr/label", a.withMr(a.labelHandler))
|
m.HandleFunc("/mr/label", a.withMr(a.labelHandler))
|
||||||
m.HandleFunc("/mr/revoke", a.withMr(a.revokeHandler))
|
m.HandleFunc("/mr/revoke", a.withMr(a.revokeHandler))
|
||||||
m.HandleFunc("/mr/awardable/note/", a.withMr(a.emojiNoteHandler))
|
m.HandleFunc("/mr/awardable/note/", a.withMr(a.emojiNoteHandler))
|
||||||
|
m.HandleFunc("/mr/draft_notes/", a.withMr(a.draftNoteHandler))
|
||||||
|
m.HandleFunc("/mr/draft_notes/publish", a.withMr(a.draftNotePublisher))
|
||||||
|
|
||||||
m.HandleFunc("/pipeline", a.pipelineHandler)
|
m.HandleFunc("/pipeline", a.pipelineHandler)
|
||||||
m.HandleFunc("/pipeline/trigger/", a.pipelineHandler)
|
m.HandleFunc("/pipeline/trigger/", a.pipelineHandler)
|
||||||
@@ -143,6 +145,7 @@ func createRouterAndApi(client ClientInterface, optFuncs ...optFunc) (*http.Serv
|
|||||||
m.HandleFunc("/job", a.jobHandler)
|
m.HandleFunc("/job", a.jobHandler)
|
||||||
m.HandleFunc("/project/members", a.projectMembersHandler)
|
m.HandleFunc("/project/members", a.projectMembersHandler)
|
||||||
m.HandleFunc("/shutdown", a.shutdownHandler)
|
m.HandleFunc("/shutdown", a.shutdownHandler)
|
||||||
|
m.HandleFunc("/merge_requests", a.mergeRequestsHandler)
|
||||||
|
|
||||||
m.Handle("/ping", http.HandlerFunc(pingHandler))
|
m.Handle("/ping", http.HandlerFunc(pingHandler))
|
||||||
|
|
||||||
|
|||||||
61
cmd/test.go
61
cmd/test.go
@@ -19,10 +19,10 @@ The FakeHandlerClient is used to create a fake gitlab client for testing our han
|
|||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
createMrFn func(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
createMrFn func(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||||
getMergeRequestFn func(pid interface{}, mergeRequestIID int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
getMergeRequest func(pid interface{}, mergeRequestIID int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||||
updateMergeRequestFn func(pid interface{}, mergeRequestIID int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
updateMergeRequest func(pid interface{}, mergeRequestIID int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||||
acceptAndMergeFn func(pid interface{}, mergeRequestIID int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
acceptMergeRequest func(pid interface{}, mergeRequestIID int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||||
unapprorveMergeRequestFn func(pid interface{}, mergeRequestIID int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
unapproveMergeRequest func(pid interface{}, mergeRequestIID int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
uploadFile func(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error)
|
uploadFile func(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error)
|
||||||
getMergeRequestDiffVersions func(pid interface{}, mergeRequestIID int, opt *gitlab.GetMergeRequestDiffVersionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequestDiffVersion, *gitlab.Response, error)
|
getMergeRequestDiffVersions func(pid interface{}, mergeRequestIID int, opt *gitlab.GetMergeRequestDiffVersionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequestDiffVersion, *gitlab.Response, error)
|
||||||
approveMergeRequest func(pid interface{}, mergeRequestIID int, opt *gitlab.ApproveMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequestApprovals, *gitlab.Response, error)
|
approveMergeRequest func(pid interface{}, mergeRequestIID int, opt *gitlab.ApproveMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequestApprovals, *gitlab.Response, error)
|
||||||
@@ -41,6 +41,13 @@ type fakeClient struct {
|
|||||||
listMergeRequestAwardEmojiOnNote func(pid interface{}, mergeRequestIID, noteID int, opt *gitlab.ListAwardEmojiOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.AwardEmoji, *gitlab.Response, error)
|
listMergeRequestAwardEmojiOnNote func(pid interface{}, mergeRequestIID, noteID int, opt *gitlab.ListAwardEmojiOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.AwardEmoji, *gitlab.Response, error)
|
||||||
deleteMergeRequestAwardEmojiOnNote func(pid interface{}, mergeRequestIID, noteID, awardID int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
deleteMergeRequestAwardEmojiOnNote func(pid interface{}, mergeRequestIID, noteID, awardID int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
currentUser func(options ...gitlab.RequestOptionFunc) (*gitlab.User, *gitlab.Response, error)
|
currentUser func(options ...gitlab.RequestOptionFunc) (*gitlab.User, *gitlab.Response, error)
|
||||||
|
createDraftNote func(pid interface{}, mergeRequestIID int, opt *gitlab.CreateDraftNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.DraftNote, *gitlab.Response, error)
|
||||||
|
listDraftNotes func(pid interface{}, mergeRequest int, opt *gitlab.ListDraftNotesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.DraftNote, *gitlab.Response, error)
|
||||||
|
deleteDraftNote func(pid interface{}, mergeRequest int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
updateDraftNote func(pid interface{}, mergeRequest int, note int, opt *gitlab.UpdateDraftNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.DraftNote, *gitlab.Response, error)
|
||||||
|
publishAllDraftNotes func(pid interface{}, mergeRequest int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
publishDraftNote func(pid interface{}, mergeRequest int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
listProjectMergeRequests func(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Author struct {
|
type Author struct {
|
||||||
@@ -58,19 +65,19 @@ func (f fakeClient) CreateMergeRequest(pid interface{}, opt *gitlab.CreateMergeR
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeClient) AcceptMergeRequest(pid interface{}, mergeRequestIID int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
func (f fakeClient) AcceptMergeRequest(pid interface{}, mergeRequestIID int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
return f.acceptAndMergeFn(pid, mergeRequestIID, opt, options...)
|
return f.acceptMergeRequest(pid, mergeRequestIID, opt, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeClient) GetMergeRequest(pid interface{}, mergeRequestIID int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
func (f fakeClient) GetMergeRequest(pid interface{}, mergeRequestIID int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
return f.getMergeRequestFn(pid, mergeRequestIID, opt, options...)
|
return f.getMergeRequest(pid, mergeRequestIID, opt, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeClient) UpdateMergeRequest(pid interface{}, mergeRequestIID int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
func (f fakeClient) UpdateMergeRequest(pid interface{}, mergeRequestIID int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
return f.updateMergeRequestFn(pid, mergeRequestIID, opt, options...)
|
return f.updateMergeRequest(pid, mergeRequestIID, opt, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeClient) UnapproveMergeRequest(pid interface{}, mergeRequestIID int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
func (f fakeClient) UnapproveMergeRequest(pid interface{}, mergeRequestIID int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
return f.unapprorveMergeRequestFn(pid, mergeRequestIID, options...)
|
return f.unapproveMergeRequest(pid, mergeRequestIID, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeClient) UploadFile(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error) {
|
func (f fakeClient) UploadFile(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error) {
|
||||||
@@ -141,19 +148,47 @@ func (f fakeClient) DeleteMergeRequestAwardEmojiOnNote(pid interface{}, mergeReq
|
|||||||
return f.deleteMergeRequestAwardEmojiOnNote(pid, mergeRequestIID, noteID, awardID)
|
return f.deleteMergeRequestAwardEmojiOnNote(pid, mergeRequestIID, noteID, awardID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) CreateDraftNote(pid interface{}, mergeRequestIID int, opt *gitlab.CreateDraftNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.DraftNote, *gitlab.Response, error) {
|
||||||
|
return f.createDraftNote(pid, mergeRequestIID, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) ListDraftNotes(pid interface{}, mergeRequestIID int, opt *gitlab.ListDraftNotesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.DraftNote, *gitlab.Response, error) {
|
||||||
|
return f.listDraftNotes(pid, mergeRequestIID, opt)
|
||||||
|
}
|
||||||
|
|
||||||
func (f fakeClient) CurrentUser(options ...gitlab.RequestOptionFunc) (*gitlab.User, *gitlab.Response, error) {
|
func (f fakeClient) CurrentUser(options ...gitlab.RequestOptionFunc) (*gitlab.User, *gitlab.Response, error) {
|
||||||
return f.currentUser()
|
return f.currentUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f fakeClient) CreateMergeRequestAwardEmojiOnNote(pid interface{}, mergeRequestIID, noteID int, opt *gitlab.CreateAwardEmojiOptions, options ...gitlab.RequestOptionFunc) (*gitlab.AwardEmoji, *gitlab.Response, error) {
|
func (f fakeClient) CreateMergeRequestAwardEmojiOnNote(pid interface{}, mergeRequestIID, noteID int, opt *gitlab.CreateAwardEmojiOptions, options ...gitlab.RequestOptionFunc) (*gitlab.AwardEmoji, *gitlab.Response, error) {
|
||||||
return &gitlab.AwardEmoji{}, &gitlab.Response{}, nil
|
return &gitlab.AwardEmoji{}, &gitlab.Response{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) UpdateDraftNote(pid interface{}, mergeRequest int, note int, opt *gitlab.UpdateDraftNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.DraftNote, *gitlab.Response, error) {
|
||||||
|
return f.updateDraftNote(pid, mergeRequest, note, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) DeleteDraftNote(pid interface{}, mergeRequestIID int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return f.deleteDraftNote(pid, mergeRequestIID, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) PublishDraftNote(pid interface{}, mergeRequest int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return f.publishDraftNote(pid, mergeRequest, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) PublishAllDraftNotes(pid interface{}, mergeRequest int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return f.publishAllDraftNotes(pid, mergeRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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) {
|
||||||
|
if f.listProjectMergeRequests == nil {
|
||||||
|
return []*gitlab.MergeRequest{{ID: 1}}, &gitlab.Response{}, nil
|
||||||
|
} else {
|
||||||
|
return f.listProjectMergeRequests(pid, opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* The assert function is a helper function used to check two comparables */
|
/* The assert function is a helper function used to check two comparables */
|
||||||
func assert[T comparable](t *testing.T, got T, want T) {
|
func assert[T comparable](t *testing.T, got T, want T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ type ClientInterface interface {
|
|||||||
CreateMergeRequestDiscussion(pid interface{}, mergeRequestIID int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
CreateMergeRequestDiscussion(pid interface{}, mergeRequestIID int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
||||||
UpdateMergeRequestDiscussionNote(pid interface{}, mergeRequestIID int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error)
|
UpdateMergeRequestDiscussionNote(pid interface{}, mergeRequestIID int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error)
|
||||||
DeleteMergeRequestDiscussionNote(pid interface{}, mergeRequestIID int, discussion string, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
DeleteMergeRequestDiscussionNote(pid interface{}, mergeRequestIID int, discussion string, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
CreateDraftNote(pid interface{}, mergeRequestIID int, opt *gitlab.CreateDraftNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.DraftNote, *gitlab.Response, error)
|
||||||
|
ListDraftNotes(pid interface{}, mergeRequest int, opt *gitlab.ListDraftNotesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.DraftNote, *gitlab.Response, error)
|
||||||
|
DeleteDraftNote(pid interface{}, mergeRequest int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
UpdateDraftNote(pid interface{}, mergeRequest int, note int, opt *gitlab.UpdateDraftNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.DraftNote, *gitlab.Response, error)
|
||||||
|
PublishDraftNote(pid interface{}, mergeRequest int, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
PublishAllDraftNotes(pid interface{}, mergeRequest int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
AddMergeRequestDiscussionNote(pid interface{}, mergeRequestIID int, discussion string, opt *gitlab.AddMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error)
|
AddMergeRequestDiscussionNote(pid interface{}, mergeRequestIID int, discussion string, opt *gitlab.AddMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error)
|
||||||
ListAllProjectMembers(pid interface{}, opt *gitlab.ListProjectMembersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectMember, *gitlab.Response, error)
|
ListAllProjectMembers(pid interface{}, opt *gitlab.ListProjectMembersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectMember, *gitlab.Response, error)
|
||||||
RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error)
|
RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error)
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ QUICK START *gitlab.nvim.quick-start*
|
|||||||
|
|
||||||
1. Install Go
|
1. Install Go
|
||||||
2. Add configuration (see Installation section)
|
2. Add configuration (see Installation section)
|
||||||
3. Checkout your feature branch: `git checkout feature-branch`
|
5. Run `:lua require("gitlab").choose_merge_request()`
|
||||||
4. Open Neovim
|
|
||||||
5. Run `:lua require("gitlab").review()` to open the reviewer pane
|
This will checkout the branch locally, and up the plugin's reviewer pane.
|
||||||
|
|
||||||
|
|
||||||
INSTALLATION *gitlab.nvim.installation*
|
INSTALLATION *gitlab.nvim.installation*
|
||||||
@@ -78,20 +78,25 @@ With Lazy:
|
|||||||
<
|
<
|
||||||
And with Packer:
|
And with Packer:
|
||||||
>lua
|
>lua
|
||||||
use {
|
use {
|
||||||
'harrisoncramer/gitlab.nvim',
|
"harrisoncramer/gitlab.nvim",
|
||||||
requires = {
|
requires = {
|
||||||
"MunifTanjim/nui.nvim",
|
"MunifTanjim/nui.nvim",
|
||||||
"nvim-lua/plenary.nvim",
|
"nvim-lua/plenary.nvim",
|
||||||
"sindrets/diffview.nvim",
|
"sindrets/diffview.nvim"
|
||||||
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
|
||||||
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
|
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
|
||||||
},
|
},
|
||||||
run = function() require("gitlab.server").build(true) end,
|
build = function()
|
||||||
config = function()
|
require("gitlab.server").build()
|
||||||
require("gitlab").setup()
|
end,
|
||||||
end,
|
branch = "develop",
|
||||||
}
|
config = function()
|
||||||
|
require("diffview") -- We require some global state from diffview
|
||||||
|
local gitlab = require("gitlab")
|
||||||
|
gitlab.setup()
|
||||||
|
end,
|
||||||
|
}
|
||||||
<
|
<
|
||||||
|
|
||||||
CONNECTING TO GITLAB *gitlab.nvim.connecting-to-gitlab*
|
CONNECTING TO GITLAB *gitlab.nvim.connecting-to-gitlab*
|
||||||
@@ -122,6 +127,22 @@ directory that holds your `.gitlab.nvim` file.
|
|||||||
The `connection_settings` block in the `state.lua` file will be used to
|
The `connection_settings` block in the `state.lua` file will be used to
|
||||||
configure your connection to Gitlab.
|
configure your connection to Gitlab.
|
||||||
|
|
||||||
|
In case even more control over the auth config is needed, there is the
|
||||||
|
possibility to override the `auth_provider` settings field. It should be
|
||||||
|
a function that returns the `token` as well as the `gitlab_url` value and
|
||||||
|
a nilable error value.
|
||||||
|
|
||||||
|
If the `gitlab_url` is `nil`, `https://gitlab.com` is used as default.
|
||||||
|
|
||||||
|
Here an example how to use a custom `auth_provider`:
|
||||||
|
>lua
|
||||||
|
require("gitlab").setup({
|
||||||
|
auth_provider = function()
|
||||||
|
return "my_token", "https://custom.gitlab.instance.url", nil
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
<
|
||||||
|
|
||||||
|
|
||||||
CONFIGURING THE PLUGIN *gitlab.nvim.configuring-the-plugin*
|
CONFIGURING THE PLUGIN *gitlab.nvim.configuring-the-plugin*
|
||||||
|
|
||||||
@@ -175,6 +196,7 @@ you call this function with no values the defaults will be used:
|
|||||||
toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions
|
toggle_unresolved_discussions = "U", -- Open or close all unresolved discussions
|
||||||
keep_current_open = false, -- If true, current discussion stays open even if it should otherwise be closed when toggling
|
keep_current_open = false, -- If true, current discussion stays open even if it should otherwise be closed when toggling
|
||||||
toggle_resolved = "p" -- Toggles the resolved status of the whole discussion
|
toggle_resolved = "p" -- Toggles the resolved status of the whole discussion
|
||||||
|
publish_draft = "P", -- Publishes the currently focused note/comment
|
||||||
position = "left", -- "top", "right", "bottom" or "left"
|
position = "left", -- "top", "right", "bottom" or "left"
|
||||||
open_in_browser = "b" -- Jump to the URL of the current note/discussion
|
open_in_browser = "b" -- Jump to the URL of the current note/discussion
|
||||||
copy_node_url = "u", -- Copy the URL of the current node to clipboard
|
copy_node_url = "u", -- Copy the URL of the current node to clipboard
|
||||||
@@ -184,9 +206,14 @@ you call this function with no values the defaults will be used:
|
|||||||
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
|
tree_type = "simple", -- Type of discussion tree - "simple" means just list of discussions, "by_file_name" means file tree with discussions under file
|
||||||
toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
|
toggle_tree_type = "i", -- Toggle type of discussion tree - "simple", or "by_file_name"
|
||||||
|
draft_mode = false, -- Whether comments are posted as drafts as part of a review
|
||||||
|
toggle_draft_mode = "D" -- Toggle between draft mode and regular mode, where comments are posted immediately
|
||||||
winbar = nil -- Custom function to return winbar title, should return a string. Provided with WinbarTable (defined in annotations.lua)
|
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.
|
-- If using lualine, please add "gitlab" to disabled file types, otherwise you will not see the winbar.
|
||||||
},
|
},
|
||||||
|
choose_merge_request = {
|
||||||
|
open_reviewer = true, -- Open the reviewer window automatically after switching merge requests
|
||||||
|
},
|
||||||
info = { -- Show additional fields in the summary view
|
info = { -- Show additional fields in the summary view
|
||||||
enabled = true,
|
enabled = true,
|
||||||
horizontal = false, -- Display metadata to the left of the summary rather than underneath
|
horizontal = false, -- Display metadata to the left of the summary rather than underneath
|
||||||
@@ -297,6 +324,16 @@ code block with prefilled code from the visual selection.
|
|||||||
Just like the summary, all the different kinds of comments are saved via the
|
Just like the summary, all the different kinds of comments are saved via the
|
||||||
`settings.popup.perform_action` keybinding.
|
`settings.popup.perform_action` keybinding.
|
||||||
|
|
||||||
|
DRAFT NOTES *gitlab.nvim.draft-comments*
|
||||||
|
|
||||||
|
When you publish a "draft" of any of the above resources (configurable via the
|
||||||
|
`state.settings.comments.default_to_draft` setting) the comment will be added
|
||||||
|
to a review. You may publish all draft comments via the `gitlab.publish_all_drafts()`
|
||||||
|
function, and you can publish an individual comment or note by pressing the
|
||||||
|
`state.settings.discussion_tree.publish_draft` keybinding.
|
||||||
|
|
||||||
|
Draft notes do not support editing, replying, or emojis.
|
||||||
|
|
||||||
TEMPORARY REGISTERS *gitlab.nvim.temp-registers*
|
TEMPORARY REGISTERS *gitlab.nvim.temp-registers*
|
||||||
|
|
||||||
While writing a note/comment/suggestion/reply, you may need to interrupt the
|
While writing a note/comment/suggestion/reply, you may need to interrupt the
|
||||||
@@ -364,7 +401,7 @@ These labels will be visible in the summary panel, as long as you provide the
|
|||||||
|
|
||||||
SIGNS AND DIAGNOSTICS *gitlab.nvim.signs-and-diagnostics*
|
SIGNS AND DIAGNOSTICS *gitlab.nvim.signs-and-diagnostics*
|
||||||
|
|
||||||
By default when reviewing files, you will see diagnostics for comments that
|
By default when reviewing files, you will see diagnostics for comments that
|
||||||
have been added to a review. These are the default settings:
|
have been added to a review. These are the default settings:
|
||||||
>lua
|
>lua
|
||||||
discussion_signs = {
|
discussion_signs = {
|
||||||
@@ -379,7 +416,7 @@ have been added to a review. These are the default settings:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
When the cursor is on diagnostic line you can view discussion thread by using `vim.diagnostic.show()`
|
When the cursor is on diagnostic line you can view discussion thread by using `vim.diagnostic.show()`
|
||||||
|
|
||||||
You can also jump to discussion tree for the given comment:
|
You can also jump to discussion tree for the given comment:
|
||||||
>lua
|
>lua
|
||||||
@@ -527,6 +564,7 @@ in normal mode):
|
|||||||
vim.keymap.set("n", "glo", gitlab.open_in_browser)
|
vim.keymap.set("n", "glo", gitlab.open_in_browser)
|
||||||
vim.keymap.set("n", "glM", gitlab.merge)
|
vim.keymap.set("n", "glM", gitlab.merge)
|
||||||
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
|
vim.keymap.set("n", "glu", gitlab.copy_mr_url)
|
||||||
|
vim.keymap.set("n", "glP", gitlab.publish_all_drafts)
|
||||||
<
|
<
|
||||||
|
|
||||||
TROUBLESHOOTING *gitlab.nvim.troubleshooting*
|
TROUBLESHOOTING *gitlab.nvim.troubleshooting*
|
||||||
@@ -567,6 +605,21 @@ default arguments outlined under "Configuring the Plugin".
|
|||||||
require("gitlab").setup({ port = 8392 })
|
require("gitlab").setup({ port = 8392 })
|
||||||
|
|
||||||
require("gitlab").setup({ discussion_tree = { blacklist = { "some_bot"} } })
|
require("gitlab").setup({ discussion_tree = { blacklist = { "some_bot"} } })
|
||||||
|
<
|
||||||
|
*gitlab.nvim.choose_merge_request*
|
||||||
|
gitlab.choose_merge_request({opts}) ~
|
||||||
|
|
||||||
|
Choose a merge request from a list of those open in your current project to review.
|
||||||
|
This command will automatically check out that branch locally, and optionally
|
||||||
|
open the reviewer pane. This is the default behavior.
|
||||||
|
>lua
|
||||||
|
require("gitlab").choose_merge_request()
|
||||||
|
require("gitlab").choose_merge_request({ open_reviewer = false })
|
||||||
|
<
|
||||||
|
Parameters: ~
|
||||||
|
• {opts}: (table|nil) Keyword arguments to configure the checkout.
|
||||||
|
• {open_reviewer}: (boolean) Whether to open the reviewer after
|
||||||
|
switching branches. True by default.
|
||||||
<
|
<
|
||||||
*gitlab.nvim.review*
|
*gitlab.nvim.review*
|
||||||
gitlab.review() ~
|
gitlab.review() ~
|
||||||
@@ -708,6 +761,14 @@ Once the discussion tree is open, a number of different keybindings are availabl
|
|||||||
for interacting with different discussions. Please see the `settings.discussion_tree`
|
for interacting with different discussions. Please see the `settings.discussion_tree`
|
||||||
section of the setup call for more information about different keybindings.
|
section of the setup call for more information about different keybindings.
|
||||||
|
|
||||||
|
*gitlab.nvim.publish_all_drafts*
|
||||||
|
gitlab.publish_all_drafts() ~
|
||||||
|
|
||||||
|
Publishes all unpublished draft notes. Used to finish a review and make all notes and
|
||||||
|
comments visible.
|
||||||
|
>lua
|
||||||
|
require("gitlab").publish_all_drafts()
|
||||||
|
<
|
||||||
*gitlab.nvim.add_assignee*
|
*gitlab.nvim.add_assignee*
|
||||||
gitlab.add_assignee() ~
|
gitlab.add_assignee() ~
|
||||||
|
|
||||||
@@ -829,6 +890,7 @@ execute and passed the data as an argument.
|
|||||||
• "pipeline": Information about the current branch's
|
• "pipeline": Information about the current branch's
|
||||||
pipeline. Returns and object with `latest_pipeline` and
|
pipeline. Returns and object with `latest_pipeline` and
|
||||||
`jobs` as fields.
|
`jobs` as fields.
|
||||||
|
• "draft_notes": The current user's unpublished notes
|
||||||
• {refresh}: (bool) Whether to re-fetch the data from Gitlab
|
• {refresh}: (bool) Whether to re-fetch the data from Gitlab
|
||||||
or use the cached data locally, if available.
|
or use the cached data locally, if available.
|
||||||
• {cb}: (function) The callback function that runs after all of the
|
• {cb}: (function) The callback function that runs after all of the
|
||||||
|
|||||||
@@ -1,126 +1,46 @@
|
|||||||
-- This module is responsible for creating new comments
|
--- This module is responsible for creating new comments
|
||||||
-- in the reviewer's buffer. The reviewer will pass back
|
--- in the reviewer's buffer. The reviewer will pass back
|
||||||
-- to this module the data required to make the API calls
|
--- to this module the data required to make the API calls
|
||||||
local Popup = require("nui.popup")
|
local Popup = require("nui.popup")
|
||||||
|
local Layout = require("nui.layout")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local job = require("gitlab.job")
|
local job = require("gitlab.job")
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local git = require("gitlab.git")
|
local git = require("gitlab.git")
|
||||||
local discussions = require("gitlab.actions.discussions")
|
local discussions = require("gitlab.actions.discussions")
|
||||||
|
local draft_notes = require("gitlab.actions.draft_notes")
|
||||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||||
local reviewer = require("gitlab.reviewer")
|
local reviewer = require("gitlab.reviewer")
|
||||||
local Location = require("gitlab.reviewer.location")
|
local Location = require("gitlab.reviewer.location")
|
||||||
local M = {}
|
|
||||||
|
|
||||||
-- Popup creation is wrapped in a function so that it is performed *after* user
|
local M = {
|
||||||
-- configuration has been merged with default configuration, not when this file is being
|
current_win = nil,
|
||||||
-- required.
|
start_line = nil,
|
||||||
local function create_comment_popup()
|
end_line = nil,
|
||||||
return Popup(u.create_popup_state("Comment", state.settings.popup.comment))
|
}
|
||||||
end
|
|
||||||
|
|
||||||
-- This function will open a comment popup in order to create a comment on the changed/updated
|
---Fires the API that sends the comment data to the Go server, called when you "confirm" creation
|
||||||
-- line in the current MR
|
---via the M.settings.popup.perform_action keybinding
|
||||||
M.create_comment = function()
|
|
||||||
local has_clean_tree = git.has_clean_tree()
|
|
||||||
local is_modified = vim.api.nvim_buf_get_option(0, "modified")
|
|
||||||
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
|
|
||||||
u.notify(
|
|
||||||
"Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local comment_popup = create_comment_popup()
|
|
||||||
comment_popup:mount()
|
|
||||||
state.set_popup_keymaps(comment_popup, function(text)
|
|
||||||
M.confirm_create_comment(text)
|
|
||||||
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Create multiline comment for the last selection.
|
|
||||||
M.create_multiline_comment = function()
|
|
||||||
if not u.check_visual_mode() then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local comment_popup = create_comment_popup()
|
|
||||||
local start_line, end_line = u.get_visual_selection_boundaries()
|
|
||||||
comment_popup:mount()
|
|
||||||
state.set_popup_keymaps(comment_popup, function(text)
|
|
||||||
M.confirm_create_comment(text, { start_line = start_line, end_line = end_line })
|
|
||||||
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Create comment prepopulated with gitlab suggestion
|
|
||||||
---https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
|
|
||||||
M.create_comment_suggestion = function()
|
|
||||||
if not u.check_visual_mode() then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local comment_popup = create_comment_popup()
|
|
||||||
local start_line, end_line = u.get_visual_selection_boundaries()
|
|
||||||
local current_line = vim.api.nvim_win_get_cursor(0)[1]
|
|
||||||
local range = end_line - start_line
|
|
||||||
local backticks = "```"
|
|
||||||
local selected_lines = u.get_lines(start_line, end_line)
|
|
||||||
|
|
||||||
for line in ipairs(selected_lines) do
|
|
||||||
if string.match(line, "^```$") then
|
|
||||||
backticks = "````"
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local suggestion_start
|
|
||||||
if start_line == current_line then
|
|
||||||
suggestion_start = backticks .. "suggestion:-0+" .. range
|
|
||||||
elseif end_line == current_line then
|
|
||||||
suggestion_start = backticks .. "suggestion:-" .. range .. "+0"
|
|
||||||
else
|
|
||||||
-- This should never happen afaik
|
|
||||||
u.notify("Unexpected suggestion position", vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
suggestion_start = suggestion_start
|
|
||||||
local suggestion_lines = {}
|
|
||||||
table.insert(suggestion_lines, suggestion_start)
|
|
||||||
vim.list_extend(suggestion_lines, selected_lines)
|
|
||||||
table.insert(suggestion_lines, backticks)
|
|
||||||
|
|
||||||
comment_popup:mount()
|
|
||||||
vim.api.nvim_buf_set_lines(comment_popup.bufnr, 0, -1, false, suggestion_lines)
|
|
||||||
state.set_popup_keymaps(comment_popup, function(text)
|
|
||||||
if range > 0 then
|
|
||||||
M.confirm_create_comment(text, { start_line = start_line, end_line = end_line })
|
|
||||||
else
|
|
||||||
M.confirm_create_comment(text, nil)
|
|
||||||
end
|
|
||||||
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
M.create_note = function()
|
|
||||||
local note_popup = Popup(u.create_popup_state("Note", state.settings.popup.note))
|
|
||||||
note_popup:mount()
|
|
||||||
state.set_popup_keymaps(note_popup, function(text)
|
|
||||||
M.confirm_create_comment(text, nil, true)
|
|
||||||
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
---This function (settings.popup.perform_action) will send the comment to the Go server
|
|
||||||
---@param text string comment text
|
---@param text string comment text
|
||||||
---@param visual_range LineRange | nil range of visual selection or nil
|
---@param visual_range LineRange | nil range of visual selection or nil
|
||||||
---@param unlinked boolean | nil if true, the comment is not linked to a line
|
---@param unlinked boolean | nil if true, the comment is not linked to a line
|
||||||
M.confirm_create_comment = function(text, visual_range, unlinked)
|
local confirm_create_comment = function(text, visual_range, unlinked)
|
||||||
if text == nil then
|
if text == nil then
|
||||||
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
|
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local is_draft = M.draft_popup and u.string_to_bool(u.get_buffer_text(M.draft_popup.bufnr))
|
||||||
if unlinked then
|
if unlinked then
|
||||||
local body = { comment = text }
|
local body = { comment = text }
|
||||||
job.run_job("/mr/comment", "POST", body, function(data)
|
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
|
||||||
u.notify("Note created!", vim.log.levels.INFO)
|
job.run_job(endpoint, "POST", body, function(data)
|
||||||
discussions.add_discussion({ data = data, unlinked = true })
|
u.notify(is_draft and "Draft note created!" or "Note created!", vim.log.levels.INFO)
|
||||||
|
if is_draft then
|
||||||
|
draft_notes.add_draft_note({ draft_note = data.draft_note, unlinked = true })
|
||||||
|
else
|
||||||
|
discussions.add_discussion({ data = data, unlinked = true })
|
||||||
|
end
|
||||||
discussions.refresh()
|
discussions.refresh()
|
||||||
end)
|
end)
|
||||||
return
|
return
|
||||||
@@ -153,11 +73,194 @@ M.confirm_create_comment = function(text, visual_range, unlinked)
|
|||||||
line_range = location_data.line_range,
|
line_range = location_data.line_range,
|
||||||
}
|
}
|
||||||
|
|
||||||
job.run_job("/mr/comment", "POST", body, function(data)
|
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
|
||||||
u.notify("Comment created!", vim.log.levels.INFO)
|
job.run_job(endpoint, "POST", body, function(data)
|
||||||
discussions.add_discussion({ data = data, unlinked = false })
|
u.notify(is_draft and "Draft comment created!" or "Comment created!", vim.log.levels.INFO)
|
||||||
|
if is_draft then
|
||||||
|
draft_notes.add_draft_note({ draft_note = data.draft_note, unlinked = false })
|
||||||
|
else
|
||||||
|
discussions.add_discussion({ data = data, has_position = true })
|
||||||
|
end
|
||||||
discussions.refresh()
|
discussions.refresh()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@class LayoutOpts
|
||||||
|
---@field ranged boolean
|
||||||
|
---@field unlinked boolean
|
||||||
|
|
||||||
|
---This function sets up the layout and popups needed to create a comment, note and
|
||||||
|
---multi-line comment. It also sets up the basic keybindings for switching between
|
||||||
|
---window panes, and for the non-primary sections.
|
||||||
|
---@param opts LayoutOpts|nil
|
||||||
|
---@return NuiLayout
|
||||||
|
local function create_comment_layout(opts)
|
||||||
|
if opts == nil then
|
||||||
|
opts = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
M.current_win = vim.api.nvim_get_current_win()
|
||||||
|
M.comment_popup = Popup(u.create_popup_state("Comment", state.settings.popup.comment))
|
||||||
|
M.draft_popup = Popup(u.create_box_popup_state("Draft", false))
|
||||||
|
M.start_line, M.end_line = u.get_visual_selection_boundaries()
|
||||||
|
|
||||||
|
local internal_layout = Layout.Box({
|
||||||
|
Layout.Box(M.comment_popup, { grow = 1 }),
|
||||||
|
Layout.Box(M.draft_popup, { size = 3 }),
|
||||||
|
}, { dir = "col" })
|
||||||
|
|
||||||
|
local layout = Layout({
|
||||||
|
position = "50%",
|
||||||
|
relative = "editor",
|
||||||
|
size = {
|
||||||
|
width = "50%",
|
||||||
|
height = "55%",
|
||||||
|
},
|
||||||
|
}, internal_layout)
|
||||||
|
|
||||||
|
local popup_opts = {
|
||||||
|
action_before_close = true,
|
||||||
|
action_before_exit = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
miscellaneous.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
|
||||||
|
|
||||||
|
local range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
|
||||||
|
local unlinked = opts.unlinked or false
|
||||||
|
|
||||||
|
state.set_popup_keymaps(M.draft_popup, function()
|
||||||
|
local text = u.get_buffer_text(M.comment_popup.bufnr)
|
||||||
|
confirm_create_comment(text, range, unlinked)
|
||||||
|
vim.api.nvim_set_current_win(M.current_win)
|
||||||
|
end, miscellaneous.toggle_bool, popup_opts)
|
||||||
|
|
||||||
|
state.set_popup_keymaps(M.comment_popup, function(text)
|
||||||
|
confirm_create_comment(text, range, unlinked)
|
||||||
|
vim.api.nvim_set_current_win(M.current_win)
|
||||||
|
end, miscellaneous.attach_file, popup_opts)
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
local draft_mode = state.settings.discussion_tree.draft_mode
|
||||||
|
vim.api.nvim_buf_set_lines(M.draft_popup.bufnr, 0, -1, false, { u.bool_to_string(draft_mode) })
|
||||||
|
end)
|
||||||
|
|
||||||
|
return layout
|
||||||
|
end
|
||||||
|
|
||||||
|
--- This function will open a comment popup in order to create a comment on the changed/updated
|
||||||
|
--- line in the current MR
|
||||||
|
M.create_comment = function()
|
||||||
|
local has_clean_tree, err = git.has_clean_tree()
|
||||||
|
if err ~= nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local is_modified = vim.api.nvim_buf_get_option(0, "modified")
|
||||||
|
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
|
||||||
|
u.notify(
|
||||||
|
"Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not M.sha_exists() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local layout = create_comment_layout()
|
||||||
|
layout:mount()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- This function will open a multi-line comment popup in order to create a multi-line comment
|
||||||
|
--- on the changed/updated line in the current MR
|
||||||
|
M.create_multiline_comment = function()
|
||||||
|
if not u.check_visual_mode() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not M.sha_exists() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local layout = create_comment_layout({ ranged = true, unlinked = false })
|
||||||
|
layout:mount()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
|
||||||
|
--- on the changed/updated line in the current MR
|
||||||
|
M.create_note = function()
|
||||||
|
local layout = create_comment_layout({ ranged = false, unlinked = true })
|
||||||
|
layout:mount()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Given the current visually selected area of text, builds text to fill in the
|
||||||
|
---comment popup with a suggested change
|
||||||
|
---@return LineRange|nil
|
||||||
|
---@return integer
|
||||||
|
local build_suggestion = function()
|
||||||
|
local current_line = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
|
M.start_line, M.end_line = u.get_visual_selection_boundaries()
|
||||||
|
|
||||||
|
local range_length = M.end_line - M.start_line
|
||||||
|
local backticks = "```"
|
||||||
|
local selected_lines = u.get_lines(M.start_line, M.end_line)
|
||||||
|
|
||||||
|
for line in ipairs(selected_lines) do
|
||||||
|
if string.match(line, "^```$") then
|
||||||
|
backticks = "````"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local suggestion_start
|
||||||
|
if M.start_line == current_line then
|
||||||
|
suggestion_start = backticks .. "suggestion:-0+" .. range_length
|
||||||
|
elseif M.end_line == current_line then
|
||||||
|
suggestion_start = backticks .. "suggestion:-" .. range_length .. "+0"
|
||||||
|
else
|
||||||
|
--- This should never happen afaik
|
||||||
|
u.notify("Unexpected suggestion position", vim.log.levels.ERROR)
|
||||||
|
return nil, 0
|
||||||
|
end
|
||||||
|
suggestion_start = suggestion_start
|
||||||
|
local suggestion_lines = {}
|
||||||
|
table.insert(suggestion_lines, suggestion_start)
|
||||||
|
vim.list_extend(suggestion_lines, selected_lines)
|
||||||
|
table.insert(suggestion_lines, backticks)
|
||||||
|
|
||||||
|
return suggestion_lines, range_length
|
||||||
|
end
|
||||||
|
|
||||||
|
--- This function will open a a popup to create a suggestion comment
|
||||||
|
--- on the changed/updated line in the current MR
|
||||||
|
--- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
|
||||||
|
M.create_comment_suggestion = function()
|
||||||
|
if not u.check_visual_mode() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not M.sha_exists() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local suggestion_lines, range_length = build_suggestion()
|
||||||
|
|
||||||
|
local layout = create_comment_layout({ ranged = range_length > 0, unlinked = false })
|
||||||
|
layout:mount()
|
||||||
|
vim.schedule(function()
|
||||||
|
if suggestion_lines then
|
||||||
|
vim.api.nvim_buf_set_lines(M.comment_popup.bufnr, 0, -1, false, suggestion_lines)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Checks to see whether you are commenting on a valid buffer. The Diffview plugin names non-existent
|
||||||
|
---buffers as 'null'
|
||||||
|
---@return boolean
|
||||||
|
M.sha_exists = function()
|
||||||
|
if vim.fn.expand("%") == "diffview://null" then
|
||||||
|
u.notify("This file does not exist, please comment on the other buffer", vim.log.levels.ERROR)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
281
lua/gitlab/actions/common.lua
Normal file
281
lua/gitlab/actions/common.lua
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
-- This module contains code shared between at least two modules. This includes
|
||||||
|
-- actions common to multiple tree types, as well as general utility functions
|
||||||
|
-- that are specific to actions (like jumping to a file or opening a URL)
|
||||||
|
local List = require("gitlab.utils.list")
|
||||||
|
local u = require("gitlab.utils")
|
||||||
|
local reviewer = require("gitlab.reviewer")
|
||||||
|
local indicators_common = require("gitlab.indicators.common")
|
||||||
|
local common_indicators = require("gitlab.indicators.common")
|
||||||
|
local state = require("gitlab.state")
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---Build note header from note
|
||||||
|
---@param note Note|DraftNote
|
||||||
|
---@return string
|
||||||
|
M.build_note_header = function(note)
|
||||||
|
if note.note then
|
||||||
|
return "@" .. state.USER.username .. " " .. ""
|
||||||
|
end
|
||||||
|
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.switch_can_edit_bufs = function(bool, ...)
|
||||||
|
local bufnrs = { ... }
|
||||||
|
---@param v integer
|
||||||
|
for _, v in ipairs(bufnrs) do
|
||||||
|
u.switch_can_edit_buf(v, bool)
|
||||||
|
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = v })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Takes in a chunk of text separated by new line characters and returns a lua table
|
||||||
|
---@param content string
|
||||||
|
---@return table
|
||||||
|
M.build_content = function(content)
|
||||||
|
local description_lines = u.lines_into_table(content)
|
||||||
|
table.insert(description_lines, "")
|
||||||
|
return description_lines
|
||||||
|
end
|
||||||
|
|
||||||
|
M.add_empty_titles = function()
|
||||||
|
local draft_notes = require("gitlab.actions.draft_notes")
|
||||||
|
local discussions = require("gitlab.actions.discussions")
|
||||||
|
local linked, unlinked, drafts =
|
||||||
|
List.new(u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions)),
|
||||||
|
List.new(u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.unlinked_discussions)),
|
||||||
|
List.new(u.ensure_table(state.DRAFT_NOTES))
|
||||||
|
|
||||||
|
local position_drafts = drafts:filter(function(note)
|
||||||
|
return draft_notes.has_position(note)
|
||||||
|
end)
|
||||||
|
local non_positioned_drafts = drafts:filter(function(note)
|
||||||
|
return not draft_notes.has_position(note)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local fields = {
|
||||||
|
{
|
||||||
|
bufnr = discussions.linked_bufnr,
|
||||||
|
count = #linked + #position_drafts,
|
||||||
|
title = "No Discussions for this MR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bufnr = discussions.unlinked_bufnr,
|
||||||
|
count = #unlinked + #non_positioned_drafts,
|
||||||
|
title = "No Notes (Unlinked Discussions) for this MR",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v in ipairs(fields) do
|
||||||
|
if v.bufnr ~= nil then
|
||||||
|
M.switch_can_edit_bufs(true, v.bufnr)
|
||||||
|
local ns_id = vim.api.nvim_create_namespace("GitlabNamespace")
|
||||||
|
vim.cmd("highlight default TitleHighlight guifg=#787878")
|
||||||
|
|
||||||
|
-- Set empty title if applicable
|
||||||
|
if v.count == 0 then
|
||||||
|
vim.api.nvim_buf_set_lines(v.bufnr, 0, 1, false, { v.title })
|
||||||
|
local linnr = 1
|
||||||
|
vim.api.nvim_buf_set_extmark(
|
||||||
|
v.bufnr,
|
||||||
|
ns_id,
|
||||||
|
linnr - 1,
|
||||||
|
0,
|
||||||
|
{ end_row = linnr - 1, end_col = string.len(v.title), hl_group = "TitleHighlight" }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param tree NuiTree
|
||||||
|
M.get_url = 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
|
||||||
|
return url
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param tree NuiTree
|
||||||
|
M.open_in_browser = function(tree)
|
||||||
|
local url = M.get_url(tree)
|
||||||
|
if url ~= nil then
|
||||||
|
u.open_in_browser(url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param tree NuiTree
|
||||||
|
M.copy_node_url = function(tree)
|
||||||
|
local url = M.get_url(tree)
|
||||||
|
if url == nil then
|
||||||
|
vim.fn.setreg("+", url)
|
||||||
|
u.notify("Copied '" .. url .. "' to clipboard", vim.log.levels.INFO)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For developers!
|
||||||
|
M.print_node = function(tree)
|
||||||
|
local current_node = tree:get_node()
|
||||||
|
vim.print(current_node)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Check if type of node is note or note body
|
||||||
|
---@param node NuiTree.Node?
|
||||||
|
---@return boolean
|
||||||
|
M.is_node_note = function(node)
|
||||||
|
if node and (node.type == "note_body" or node.type == "note") then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get root node
|
||||||
|
---@param tree NuiTree
|
||||||
|
---@param node NuiTree.Node?
|
||||||
|
---@return NuiTree.Node?
|
||||||
|
M.get_root_node = function(tree, node)
|
||||||
|
if not node then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if node.type == "note_body" or node.type == "note" and not node.is_root then
|
||||||
|
local parent_id = node:get_parent_id()
|
||||||
|
return M.get_root_node(tree, tree:get_node(parent_id))
|
||||||
|
elseif node.is_root then
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get note node
|
||||||
|
---@param tree NuiTree
|
||||||
|
---@param node NuiTree.Node?
|
||||||
|
---@return NuiTree.Node?
|
||||||
|
M.get_note_node = function(tree, node)
|
||||||
|
if not node then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if node.type == "note_body" then
|
||||||
|
local parent_id = node:get_parent_id()
|
||||||
|
if parent_id == nil then
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
return M.get_note_node(tree, tree:get_node(parent_id))
|
||||||
|
elseif node.type == "note" then
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Takes a node and returns the line where the note is positioned in the new SHA. If
|
||||||
|
---the line is not in the new SHA, returns nil
|
||||||
|
---@param node NuiTree.Node
|
||||||
|
---@return number|nil
|
||||||
|
local function get_new_line(node)
|
||||||
|
---@type GitlabLineRange|nil
|
||||||
|
local range = node.range
|
||||||
|
if range == nil then
|
||||||
|
return node.new_line
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, start_new_line = common_indicators.parse_line_code(range.start.line_code)
|
||||||
|
return start_new_line
|
||||||
|
end
|
||||||
|
|
||||||
|
---Takes a node and returns the line where the note is positioned in the old SHA. If
|
||||||
|
---the line is not in the old SHA, returns nil
|
||||||
|
---@param node NuiTree.Node
|
||||||
|
---@return number|nil
|
||||||
|
local function get_old_line(node)
|
||||||
|
---@type GitlabLineRange|nil
|
||||||
|
local range = node.range
|
||||||
|
if range == nil then
|
||||||
|
return node.old_line
|
||||||
|
end
|
||||||
|
|
||||||
|
local start_old_line, _ = common_indicators.parse_line_code(range.start.line_code)
|
||||||
|
return start_old_line
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param id string|integer
|
||||||
|
---@return integer|nil
|
||||||
|
M.get_line_number = function(id)
|
||||||
|
---@type Discussion|DraftNote|nil
|
||||||
|
local d_or_n
|
||||||
|
d_or_n = List.new(state.DISCUSSION_DATA.discussions or {}):find(function(d)
|
||||||
|
return d.id == id
|
||||||
|
end) or List.new(state.DRAFT_NOTES or {}):find(function(d)
|
||||||
|
return d.id == id
|
||||||
|
end)
|
||||||
|
|
||||||
|
if d_or_n == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local first_note = indicators_common.get_first_note(d_or_n)
|
||||||
|
return (indicators_common.is_new_sha(d_or_n) and first_note.position.new_line or first_note.position.old_line) or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param root_node NuiTree.Node
|
||||||
|
---@return integer|nil
|
||||||
|
M.get_line_number_from_node = function(root_node)
|
||||||
|
if root_node.range then
|
||||||
|
local start_old_line, start_new_line = common_indicators.parse_line_code(root_node.range.start.line_code)
|
||||||
|
return root_node.old_line and start_old_line or start_new_line
|
||||||
|
else
|
||||||
|
return M.get_line_number(root_node.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This function (settings.discussion_tree.jump_to_reviewer) will jump the cursor to the reviewer's location associated with the note. The implementation depends on the reviewer
|
||||||
|
M.jump_to_reviewer = function(tree, callback)
|
||||||
|
local node = tree:get_node()
|
||||||
|
local root_node = M.get_root_node(tree, node)
|
||||||
|
if root_node == nil then
|
||||||
|
u.notify("Could not get discussion node", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local line_number = M.get_line_number_from_node(root_node)
|
||||||
|
if line_number == nil then
|
||||||
|
u.notify("Could not get line number", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
reviewer.jump(root_node.file_name, line_number, root_node.old_line == nil)
|
||||||
|
callback()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This function (settings.discussion_tree.jump_to_file) will jump to the file changed in a new tab
|
||||||
|
M.jump_to_file = function(tree)
|
||||||
|
local node = tree:get_node()
|
||||||
|
local root_node = M.get_root_node(tree, node)
|
||||||
|
if root_node == nil then
|
||||||
|
u.notify("Could not get discussion node", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if root_node.file_name == nil then
|
||||||
|
u.notify("This comment was not left on a particular location", vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.cmd.tabnew()
|
||||||
|
local line_number = get_new_line(root_node) or get_old_line(root_node)
|
||||||
|
if line_number == nil then
|
||||||
|
line_number = 1
|
||||||
|
end
|
||||||
|
local bufnr = vim.fn.bufnr(root_node.file_name)
|
||||||
|
if bufnr ~= -1 then
|
||||||
|
vim.cmd("buffer " .. bufnr)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If buffer is not already open, open it
|
||||||
|
vim.cmd("edit " .. root_node.file_name)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -7,6 +7,7 @@ local job = require("gitlab.job")
|
|||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local git = require("gitlab.git")
|
local git = require("gitlab.git")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
|
local common = require("gitlab.actions.common")
|
||||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||||
|
|
||||||
---@class Mr
|
---@class Mr
|
||||||
@@ -42,6 +43,10 @@ end
|
|||||||
--- continue working on it.
|
--- continue working on it.
|
||||||
---@param args? Mr
|
---@param args? Mr
|
||||||
M.start = function(args)
|
M.start = function(args)
|
||||||
|
if not git.current_branch_up_to_date_on_remote(vim.log.levels.ERROR) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if M.started then
|
if M.started then
|
||||||
vim.ui.select({ "Yes", "No" }, { prompt = "Continue your previous MR?" }, function(choice)
|
vim.ui.select({ "Yes", "No" }, { prompt = "Continue your previous MR?" }, function(choice)
|
||||||
if choice == "Yes" then
|
if choice == "Yes" then
|
||||||
@@ -82,7 +87,10 @@ M.pick_target = function(mr)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function make_template_path(t)
|
local function make_template_path(t)
|
||||||
local base_dir = git.base_dir()
|
local base_dir, err = git.base_dir()
|
||||||
|
if err ~= nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
return base_dir
|
return base_dir
|
||||||
.. state.settings.file_separator
|
.. state.settings.file_separator
|
||||||
.. ".gitlab"
|
.. ".gitlab"
|
||||||
@@ -202,7 +210,7 @@ M.open_confirmation_popup = function(mr)
|
|||||||
M.layout_visible = false
|
M.layout_visible = false
|
||||||
end
|
end
|
||||||
|
|
||||||
local description_lines = mr.description and M.build_description_lines(mr.description) or { "" }
|
local description_lines = mr.description and common.build_content(mr.description) or { "" }
|
||||||
local delete_branch = u.get_first_non_nil_value({ mr.delete_branch, state.settings.create_mr.delete_branch })
|
local delete_branch = u.get_first_non_nil_value({ mr.delete_branch, state.settings.create_mr.delete_branch })
|
||||||
local squash = u.get_first_non_nil_value({ mr.squash, state.settings.create_mr.squash })
|
local squash = u.get_first_non_nil_value({ mr.squash, state.settings.create_mr.squash })
|
||||||
|
|
||||||
@@ -234,18 +242,6 @@ M.open_confirmation_popup = function(mr)
|
|||||||
end)
|
end)
|
||||||
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 u.split_by_new_lines(template_content) do
|
|
||||||
table.insert(description_lines, line)
|
|
||||||
end
|
|
||||||
-- TODO: @harrisoncramer Same as in lua/gitlab/actions/summary.lua:114
|
|
||||||
table.insert(description_lines, "")
|
|
||||||
|
|
||||||
return description_lines
|
|
||||||
end
|
|
||||||
|
|
||||||
---Prompts for interactive selection of a new target among remote-tracking branches
|
---Prompts for interactive selection of a new target among remote-tracking branches
|
||||||
M.select_new_target = function()
|
M.select_new_target = function()
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ local labels = state.dependencies.labels
|
|||||||
local project_members = state.dependencies.project_members
|
local project_members = state.dependencies.project_members
|
||||||
local revisions = state.dependencies.revisions
|
local revisions = state.dependencies.revisions
|
||||||
local latest_pipeline = state.dependencies.latest_pipeline
|
local latest_pipeline = state.dependencies.latest_pipeline
|
||||||
|
local draft_notes = state.dependencies.draft_notes
|
||||||
|
|
||||||
M.data = function(resources, cb)
|
M.data = function(resources, cb)
|
||||||
if type(resources) ~= "table" or type(cb) ~= "function" then
|
if type(resources) ~= "table" or type(cb) ~= "function" then
|
||||||
@@ -23,6 +24,7 @@ M.data = function(resources, cb)
|
|||||||
project_members = project_members,
|
project_members = project_members,
|
||||||
revisions = revisions,
|
revisions = revisions,
|
||||||
pipeline = latest_pipeline,
|
pipeline = latest_pipeline,
|
||||||
|
draft_notes = draft_notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
local api_calls = {}
|
local api_calls = {}
|
||||||
|
|||||||
@@ -79,9 +79,11 @@
|
|||||||
---@field moji string
|
---@field moji string
|
||||||
|
|
||||||
---@class WinbarTable
|
---@class WinbarTable
|
||||||
---@field name string
|
---@field view_type string
|
||||||
---@field resolvable_discussions number
|
---@field resolvable_discussions number
|
||||||
---@field resolved_discussions number
|
---@field resolved_discussions number
|
||||||
|
---@field inline_draft_notes number
|
||||||
|
---@field unlinked_draft_notes number
|
||||||
---@field resolvable_notes number
|
---@field resolvable_notes number
|
||||||
---@field resolved_notes number
|
---@field resolved_notes number
|
||||||
---@field help_keymap string
|
---@field help_keymap string
|
||||||
@@ -120,3 +122,14 @@
|
|||||||
---@field old_line integer | nil
|
---@field old_line integer | nil
|
||||||
---@field new_line integer | nil
|
---@field new_line integer | nil
|
||||||
---@field line_range ReviewerRangeInfo|nil
|
---@field line_range ReviewerRangeInfo|nil
|
||||||
|
|
||||||
|
---@class DraftNote
|
||||||
|
---@field note string
|
||||||
|
---@field id integer
|
||||||
|
---@field author_id integer
|
||||||
|
---@field merge_request_id integer
|
||||||
|
---@field resolve_discussion boolean
|
||||||
|
---@field discussion_id string -- This will always be ""
|
||||||
|
---@field commit_id string -- This will always be ""
|
||||||
|
---@field line_code string
|
||||||
|
---@field position NotePosition
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,149 +1,22 @@
|
|||||||
local state = require("gitlab.state")
|
-- This module contains tree code specific to the discussion tree, that
|
||||||
|
-- is not used in the draft notes tree
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
|
local common = require("gitlab.actions.common")
|
||||||
|
local state = require("gitlab.state")
|
||||||
local NuiTree = require("nui.tree")
|
local NuiTree = require("nui.tree")
|
||||||
|
local NuiLine = require("nui.line")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local attach_uuid = function(str)
|
|
||||||
return { text = str, id = u.uuid() }
|
|
||||||
end
|
|
||||||
|
|
||||||
---Create path node
|
|
||||||
---@param relative_path string
|
|
||||||
---@param full_path string
|
|
||||||
---@param child_nodes NuiTree.Node[]?
|
|
||||||
---@return NuiTree.Node
|
|
||||||
local function create_path_node(relative_path, full_path, child_nodes)
|
|
||||||
return NuiTree.Node({
|
|
||||||
text = relative_path,
|
|
||||||
path = full_path,
|
|
||||||
id = full_path,
|
|
||||||
type = "path",
|
|
||||||
icon = " ",
|
|
||||||
icon_hl = "GitlabDirectoryIcon",
|
|
||||||
text_hl = "GitlabDirectory",
|
|
||||||
}, child_nodes or {})
|
|
||||||
end
|
|
||||||
|
|
||||||
---Create file name node
|
|
||||||
---@param file_name string
|
|
||||||
---@param full_file_path string
|
|
||||||
---@param child_nodes NuiTree.Node[]?
|
|
||||||
---@return NuiTree.Node
|
|
||||||
local function create_file_name_node(file_name, full_file_path, child_nodes)
|
|
||||||
local icon, icon_hl = u.get_icon(file_name)
|
|
||||||
return NuiTree.Node({
|
|
||||||
text = file_name,
|
|
||||||
file_name = full_file_path,
|
|
||||||
id = full_file_path,
|
|
||||||
type = "file_name",
|
|
||||||
icon = icon,
|
|
||||||
icon_hl = icon_hl,
|
|
||||||
text_hl = "GitlabFileName",
|
|
||||||
}, child_nodes or {})
|
|
||||||
end
|
|
||||||
|
|
||||||
---Sort list of nodes (in place) of type "path" or "file_name"
|
|
||||||
---@param nodes NuiTree.Node[]
|
|
||||||
local function sort_nodes(nodes)
|
|
||||||
table.sort(nodes, function(node1, node2)
|
|
||||||
if node1.type == "path" and node2.type == "path" then
|
|
||||||
return node1.path < node2.path
|
|
||||||
elseif node1.type == "file_name" and node2.type == "file_name" then
|
|
||||||
return node1.file_name < node2.file_name
|
|
||||||
elseif node1.type == "path" and node2.type == "file_name" then
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Merge path nodes which have only single path child
|
|
||||||
---@param node NuiTree.Node
|
|
||||||
local function flatten_nodes(node)
|
|
||||||
if node.type ~= "path" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
for _, child in ipairs(node.__children) do
|
|
||||||
flatten_nodes(child)
|
|
||||||
end
|
|
||||||
if #node.__children == 1 and node.__children[1].type == "path" then
|
|
||||||
local child = node.__children[1]
|
|
||||||
node.__children = child.__children
|
|
||||||
node.id = child.id
|
|
||||||
node.path = child.path
|
|
||||||
node.text = node.text .. u.path_separator .. child.text
|
|
||||||
end
|
|
||||||
sort_nodes(node.__children)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Build note header from note.
|
|
||||||
---@param note Note
|
|
||||||
---@return string
|
|
||||||
M.build_note_header = function(note)
|
|
||||||
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Build note node body
|
|
||||||
---@param note Note
|
|
||||||
---@param resolve_info table?
|
|
||||||
---@return string
|
|
||||||
---@return NuiTree.Node[]
|
|
||||||
local function build_note_body(note, resolve_info)
|
|
||||||
local text_nodes = {}
|
|
||||||
for bodyLine in u.split_by_new_lines(note.body) do
|
|
||||||
local line = attach_uuid(bodyLine)
|
|
||||||
table.insert(
|
|
||||||
text_nodes,
|
|
||||||
NuiTree.Node({
|
|
||||||
new_line = (type(note.position) == "table" and note.position.new_line),
|
|
||||||
old_line = (type(note.position) == "table" and note.position.old_line),
|
|
||||||
text = line.text,
|
|
||||||
id = line.id,
|
|
||||||
type = "note_body",
|
|
||||||
}, {})
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
local resolve_symbol = ""
|
|
||||||
if resolve_info ~= nil and resolve_info.resolvable then
|
|
||||||
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
|
|
||||||
or state.settings.discussion_tree.unresolved
|
|
||||||
end
|
|
||||||
|
|
||||||
local noteHeader = M.build_note_header(note) .. " " .. resolve_symbol
|
|
||||||
|
|
||||||
return noteHeader, text_nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
---Build note node
|
|
||||||
---@param note Note
|
|
||||||
---@param resolve_info table?
|
|
||||||
---@return NuiTree.Node
|
|
||||||
---@return string
|
|
||||||
---@return NuiTree.Node[]
|
|
||||||
M.build_note = function(note, resolve_info)
|
|
||||||
local text, text_nodes = build_note_body(note, resolve_info)
|
|
||||||
local note_node = NuiTree.Node({
|
|
||||||
text = text,
|
|
||||||
id = note.id,
|
|
||||||
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)
|
|
||||||
|
|
||||||
return note_node, text, text_nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
---Create nodes for NuiTree from discussions
|
---Create nodes for NuiTree from discussions
|
||||||
---@param items Discussion[]
|
---@param items Discussion[]
|
||||||
---@param unlinked boolean? False or nil means that discussions are linked to code lines
|
---@param unlinked boolean? False or nil means that discussions are linked to code lines
|
||||||
---@return NuiTree.Node[]
|
---@return NuiTree.Node[]
|
||||||
M.add_discussions_to_table = function(items, unlinked)
|
M.add_discussions_to_table = function(items, unlinked)
|
||||||
local t = {}
|
local t = {}
|
||||||
|
if items == vim.NIL then
|
||||||
|
items = {}
|
||||||
|
end
|
||||||
for _, discussion in ipairs(items) do
|
for _, discussion in ipairs(items) do
|
||||||
local discussion_children = {}
|
local discussion_children = {}
|
||||||
|
|
||||||
@@ -206,10 +79,85 @@ M.add_discussions_to_table = function(items, unlinked)
|
|||||||
return t
|
return t
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return M.create_node_list_by_file_name(t)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Create path node
|
||||||
|
---@param relative_path string
|
||||||
|
---@param full_path string
|
||||||
|
---@param child_nodes NuiTree.Node[]?
|
||||||
|
---@return NuiTree.Node
|
||||||
|
local function create_path_node(relative_path, full_path, child_nodes)
|
||||||
|
return NuiTree.Node({
|
||||||
|
text = relative_path,
|
||||||
|
path = full_path,
|
||||||
|
id = full_path,
|
||||||
|
type = "path",
|
||||||
|
icon = " ",
|
||||||
|
icon_hl = "GitlabDirectoryIcon",
|
||||||
|
text_hl = "GitlabDirectory",
|
||||||
|
}, child_nodes or {})
|
||||||
|
end
|
||||||
|
|
||||||
|
---Sort list of nodes (in place) of type "path" or "file_name"
|
||||||
|
---@param nodes NuiTree.Node[]
|
||||||
|
local function sort_nodes(nodes)
|
||||||
|
table.sort(nodes, function(node1, node2)
|
||||||
|
if node1.type == "path" and node2.type == "path" then
|
||||||
|
return node1.path < node2.path
|
||||||
|
elseif node1.type == "file_name" and node2.type == "file_name" then
|
||||||
|
return node1.file_name < node2.file_name
|
||||||
|
elseif node1.type == "path" and node2.type == "file_name" then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Merge path nodes which have only single path child
|
||||||
|
---@param node NuiTree.Node
|
||||||
|
local function flatten_nodes(node)
|
||||||
|
if node.type ~= "path" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for _, child in ipairs(node.__children) do
|
||||||
|
flatten_nodes(child)
|
||||||
|
end
|
||||||
|
if #node.__children == 1 and node.__children[1].type == "path" then
|
||||||
|
local child = node.__children[1]
|
||||||
|
node.__children = child.__children
|
||||||
|
node.id = child.id
|
||||||
|
node.path = child.path
|
||||||
|
node.text = node.text .. u.path_separator .. child.text
|
||||||
|
end
|
||||||
|
sort_nodes(node.__children)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Create file name node
|
||||||
|
---@param file_name string
|
||||||
|
---@param full_file_path string
|
||||||
|
---@param child_nodes NuiTree.Node[]?
|
||||||
|
---@return NuiTree.Node
|
||||||
|
local function create_file_name_node(file_name, full_file_path, child_nodes)
|
||||||
|
local icon, icon_hl = u.get_icon(file_name)
|
||||||
|
return NuiTree.Node({
|
||||||
|
text = file_name,
|
||||||
|
file_name = full_file_path,
|
||||||
|
id = full_file_path,
|
||||||
|
type = "file_name",
|
||||||
|
icon = icon,
|
||||||
|
icon_hl = icon_hl,
|
||||||
|
text_hl = "GitlabFileName",
|
||||||
|
}, child_nodes or {})
|
||||||
|
end
|
||||||
|
|
||||||
|
local create_disscussions_by_file_name = function(node_list)
|
||||||
-- Create all the folder and file name nodes.
|
-- Create all the folder and file name nodes.
|
||||||
local discussion_by_file_name = {}
|
local discussion_by_file_name = {}
|
||||||
local top_level_path_to_node = {}
|
local top_level_path_to_node = {}
|
||||||
for _, node in ipairs(t) do
|
|
||||||
|
for _, node in ipairs(node_list) do
|
||||||
local path = ""
|
local path = ""
|
||||||
local parent_node = nil
|
local parent_node = nil
|
||||||
local path_parts = u.split_path(node.file_name)
|
local path_parts = u.split_path(node.file_name)
|
||||||
@@ -274,13 +222,280 @@ M.add_discussions_to_table = function(items, unlinked)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return discussion_by_file_name
|
||||||
|
end
|
||||||
|
|
||||||
|
M.create_node_list_by_file_name = function(node_list)
|
||||||
|
-- Create all the folder and file name nodes.
|
||||||
|
local discussion_by_file_name = create_disscussions_by_file_name(node_list)
|
||||||
|
|
||||||
-- Flatten empty folders
|
-- Flatten empty folders
|
||||||
for _, node in ipairs(discussion_by_file_name) do
|
for _, node in ipairs(discussion_by_file_name) do
|
||||||
flatten_nodes(node)
|
flatten_nodes(node)
|
||||||
end
|
end
|
||||||
|
|
||||||
sort_nodes(discussion_by_file_name)
|
sort_nodes(discussion_by_file_name)
|
||||||
|
|
||||||
return discussion_by_file_name
|
return discussion_by_file_name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local attach_uuid = function(str)
|
||||||
|
return { text = str, id = u.uuid() }
|
||||||
|
end
|
||||||
|
|
||||||
|
---Build note node body
|
||||||
|
---@param note Note|DraftNote
|
||||||
|
---@param resolve_info table?
|
||||||
|
---@return string
|
||||||
|
---@return NuiTree.Node[]
|
||||||
|
local function build_note_body(note, resolve_info)
|
||||||
|
local text_nodes = {}
|
||||||
|
for bodyLine in u.split_by_new_lines(note.body or note.note) do
|
||||||
|
local line = attach_uuid(bodyLine)
|
||||||
|
table.insert(
|
||||||
|
text_nodes,
|
||||||
|
NuiTree.Node({
|
||||||
|
new_line = (type(note.position) == "table" and note.position.new_line),
|
||||||
|
old_line = (type(note.position) == "table" and note.position.old_line),
|
||||||
|
text = line.text,
|
||||||
|
id = line.id,
|
||||||
|
type = "note_body",
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local resolve_symbol = ""
|
||||||
|
if resolve_info ~= nil and resolve_info.resolvable then
|
||||||
|
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
|
||||||
|
or state.settings.discussion_tree.unresolved
|
||||||
|
end
|
||||||
|
|
||||||
|
local noteHeader = common.build_note_header(note) .. " " .. resolve_symbol
|
||||||
|
|
||||||
|
return noteHeader, text_nodes
|
||||||
|
end
|
||||||
|
|
||||||
|
---Build note node
|
||||||
|
---@param note Note|DraftNote
|
||||||
|
---@param resolve_info table?
|
||||||
|
---@return NuiTree.Node
|
||||||
|
---@return string
|
||||||
|
---@return NuiTree.Node[]
|
||||||
|
M.build_note = function(note, resolve_info)
|
||||||
|
local text, text_nodes = build_note_body(note, resolve_info)
|
||||||
|
local note_node = NuiTree.Node({
|
||||||
|
text = text,
|
||||||
|
is_draft = note.note ~= nil,
|
||||||
|
id = note.id,
|
||||||
|
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)
|
||||||
|
|
||||||
|
return note_node, text, text_nodes
|
||||||
|
end
|
||||||
|
|
||||||
|
---Inspired by default func https://github.com/MunifTanjim/nui.nvim/blob/main/lua/nui/tree/util.lua#L38
|
||||||
|
M.nui_tree_prepare_node = function(node)
|
||||||
|
if not node.text then
|
||||||
|
error("missing node.text")
|
||||||
|
end
|
||||||
|
|
||||||
|
local texts = node.text
|
||||||
|
if type(node.text) ~= "table" or node.text.content then
|
||||||
|
texts = { node.text }
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
|
||||||
|
for i, text in ipairs(texts) do
|
||||||
|
local line = NuiLine()
|
||||||
|
|
||||||
|
line:append(string.rep(" ", node._depth - 1))
|
||||||
|
|
||||||
|
if i == 1 and node:has_children() then
|
||||||
|
line:append(node:is_expanded() and " " or " ")
|
||||||
|
if node.icon then
|
||||||
|
line:append(node.icon .. " ", node.icon_hl)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
line:append(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
line:append(text, node.text_hl)
|
||||||
|
|
||||||
|
local note_id = tostring(node.is_root and node.root_note_id or node.id)
|
||||||
|
|
||||||
|
local e = require("gitlab.emoji")
|
||||||
|
|
||||||
|
---@type Emoji[]
|
||||||
|
local emojis = state.DISCUSSION_DATA.emojis[note_id]
|
||||||
|
local placed_emojis = {}
|
||||||
|
if emojis ~= nil then
|
||||||
|
for _, v in ipairs(emojis) do
|
||||||
|
local icon = e.emoji_map[v.name]
|
||||||
|
if icon ~= nil and not u.contains(placed_emojis, icon.moji) then
|
||||||
|
line:append(" ")
|
||||||
|
line:append(icon.moji)
|
||||||
|
table.insert(placed_emojis, icon.moji)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class ToggleNodesOptions
|
||||||
|
---@field toggle_resolved boolean Whether to toggle resolved discussions.
|
||||||
|
---@field toggle_unresolved boolean Whether to toggle unresolved discussions.
|
||||||
|
---@field keep_current_open boolean Whether to keep the current discussion open even if it should otherwise be closed.
|
||||||
|
|
||||||
|
---This function (settings.discussion_tree.toggle_nodes) expands/collapses all nodes and their children according to the opts.
|
||||||
|
---@param tree NuiTree
|
||||||
|
---@param winid integer
|
||||||
|
---@param unlinked boolean
|
||||||
|
---@param opts ToggleNodesOptions
|
||||||
|
M.toggle_nodes = function(winid, tree, unlinked, opts)
|
||||||
|
local current_node = tree:get_node()
|
||||||
|
if current_node == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local root_node = common.get_root_node(tree, current_node)
|
||||||
|
for _, node in ipairs(tree:get_nodes()) do
|
||||||
|
if opts.toggle_resolved then
|
||||||
|
if
|
||||||
|
(unlinked and state.unlinked_discussion_tree.resolved_expanded)
|
||||||
|
or (not unlinked and state.discussion_tree.resolved_expanded)
|
||||||
|
then
|
||||||
|
M.collapse_recursively(tree, node, root_node, opts.keep_current_open, true)
|
||||||
|
else
|
||||||
|
M.expand_recursively(tree, node, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if opts.toggle_unresolved then
|
||||||
|
if
|
||||||
|
(unlinked and state.unlinked_discussion_tree.unresolved_expanded)
|
||||||
|
or (not unlinked and state.discussion_tree.unresolved_expanded)
|
||||||
|
then
|
||||||
|
M.collapse_recursively(tree, node, root_node, opts.keep_current_open, false)
|
||||||
|
else
|
||||||
|
M.expand_recursively(tree, node, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Reset states of resolved discussions after toggling
|
||||||
|
if opts.toggle_resolved then
|
||||||
|
if unlinked then
|
||||||
|
state.unlinked_discussion_tree.resolved_expanded = not state.unlinked_discussion_tree.resolved_expanded
|
||||||
|
else
|
||||||
|
state.discussion_tree.resolved_expanded = not state.discussion_tree.resolved_expanded
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Reset states of unresolved discussions after toggling
|
||||||
|
if opts.toggle_unresolved then
|
||||||
|
if unlinked then
|
||||||
|
state.unlinked_discussion_tree.unresolved_expanded = not state.unlinked_discussion_tree.unresolved_expanded
|
||||||
|
else
|
||||||
|
state.discussion_tree.unresolved_expanded = not state.discussion_tree.unresolved_expanded
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tree:render()
|
||||||
|
M.restore_cursor_position(winid, tree, current_node, root_node)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Restore cursor position to the original node if possible
|
||||||
|
M.restore_cursor_position = function(winid, tree, original_node, root_node)
|
||||||
|
local _, line_number = tree:get_node("-" .. tostring(original_node.id))
|
||||||
|
-- If current_node is has been collapsed, get line number of root node instead
|
||||||
|
if line_number == nil and root_node then
|
||||||
|
_, line_number = tree:get_node("-" .. tostring(root_node.id))
|
||||||
|
end
|
||||||
|
if line_number ~= nil then
|
||||||
|
vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---This function (settings.discussion_tree.expand_recursively) expands a node and its children.
|
||||||
|
---@param tree NuiTree
|
||||||
|
---@param node NuiTree.Node
|
||||||
|
---@param is_resolved boolean If true, expand resolved discussions. If false, expand unresolved discussions.
|
||||||
|
M.expand_recursively = function(tree, node, is_resolved)
|
||||||
|
if node == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if common.is_node_note(node) and common.get_root_node(tree, node).resolved == is_resolved then
|
||||||
|
node:expand()
|
||||||
|
end
|
||||||
|
local children = node:get_child_ids()
|
||||||
|
for _, child in ipairs(children) do
|
||||||
|
M.expand_recursively(tree, tree:get_node(child), is_resolved)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---This function (settings.discussion_tree.collapse_recursively) collapses a node and its children.
|
||||||
|
---@param tree NuiTree
|
||||||
|
---@param node NuiTree.Node
|
||||||
|
---@param current_root_node NuiTree.Node The root node of the current node.
|
||||||
|
---@param keep_current_open boolean If true, the current node stays open, even if it should otherwise be collapsed.
|
||||||
|
---@param is_resolved boolean If true, collapse resolved discussions. If false, collapse unresolved discussions.
|
||||||
|
M.collapse_recursively = function(tree, node, current_root_node, keep_current_open, is_resolved)
|
||||||
|
if node == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local root_node = common.get_root_node(tree, node)
|
||||||
|
if common.is_node_note(node) and root_node.resolved == is_resolved then
|
||||||
|
if keep_current_open and root_node == current_root_node then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
node:collapse()
|
||||||
|
end
|
||||||
|
local children = node:get_child_ids()
|
||||||
|
for _, child in ipairs(children) do
|
||||||
|
M.collapse_recursively(tree, tree:get_node(child), current_root_node, keep_current_open, is_resolved)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This function (settings.discussion_tree.toggle_node) expands/collapses the current node and its children
|
||||||
|
M.toggle_node = function(tree)
|
||||||
|
local node = tree:get_node()
|
||||||
|
if node == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Switch to the "note" node from "note_body" nodes to enable toggling discussions inside comments
|
||||||
|
if node.type == "note_body" then
|
||||||
|
node = tree:get_node(node:get_parent_id())
|
||||||
|
end
|
||||||
|
if node == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local children = node:get_child_ids()
|
||||||
|
if node == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if node:is_expanded() then
|
||||||
|
node:collapse()
|
||||||
|
if common.is_node_note(node) then
|
||||||
|
for _, child in ipairs(children) do
|
||||||
|
tree:get_node(child):collapse()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if common.is_node_note(node) then
|
||||||
|
for _, child in ipairs(children) do
|
||||||
|
tree:get_node(child):expand()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
node:expand()
|
||||||
|
end
|
||||||
|
|
||||||
|
tree:render()
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
local M = {}
|
|
||||||
local state = require("gitlab.state")
|
|
||||||
local List = require("gitlab.utils.list")
|
local List = require("gitlab.utils.list")
|
||||||
|
local state = require("gitlab.state")
|
||||||
|
|
||||||
|
local M = {
|
||||||
|
bufnr_map = {
|
||||||
|
discussions = nil,
|
||||||
|
notes = nil,
|
||||||
|
},
|
||||||
|
current_view_type = state.settings.discussion_tree.default_view,
|
||||||
|
}
|
||||||
|
|
||||||
|
M.set_buffers = function(linked_bufnr, unlinked_bufnr)
|
||||||
|
M.bufnr_map = {
|
||||||
|
discussions = linked_bufnr,
|
||||||
|
notes = unlinked_bufnr,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
---@param nodes Discussion[]|UnlinkedDiscussion[]|nil
|
---@param nodes Discussion[]|UnlinkedDiscussion[]|nil
|
||||||
---@return number, number
|
---@return number, number
|
||||||
@@ -30,36 +44,131 @@ local get_data = function(nodes)
|
|||||||
return total_resolvable, total_resolved
|
return total_resolvable, total_resolved
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param discussions Discussion[]|nil
|
local function content()
|
||||||
---@param unlinked_discussions UnlinkedDiscussion[]|nil
|
local resolvable_discussions, resolved_discussions = get_data(state.DISCUSSION_DATA.discussions)
|
||||||
---@param file_name string
|
local resolvable_notes, resolved_notes = get_data(state.DISCUSSION_DATA.unlinked_discussions)
|
||||||
local function content(discussions, unlinked_discussions, file_name)
|
|
||||||
local resolvable_discussions, resolved_discussions = get_data(discussions)
|
local draft_notes = require("gitlab.actions.draft_notes")
|
||||||
local resolvable_notes, resolved_notes = get_data(unlinked_discussions)
|
local inline_draft_notes = List.new(state.DRAFT_NOTES):filter(draft_notes.has_position)
|
||||||
|
local unlinked_draft_notes = List.new(state.DRAFT_NOTES):filter(function(note)
|
||||||
|
return not draft_notes.has_position(note)
|
||||||
|
end)
|
||||||
|
|
||||||
local t = {
|
local t = {
|
||||||
name = file_name,
|
|
||||||
resolvable_discussions = resolvable_discussions,
|
resolvable_discussions = resolvable_discussions,
|
||||||
resolved_discussions = resolved_discussions,
|
resolved_discussions = resolved_discussions,
|
||||||
|
inline_draft_notes = #inline_draft_notes,
|
||||||
|
unlinked_draft_notes = #unlinked_draft_notes,
|
||||||
resolvable_notes = resolvable_notes,
|
resolvable_notes = resolvable_notes,
|
||||||
resolved_notes = resolved_notes,
|
resolved_notes = resolved_notes,
|
||||||
help_keymap = state.settings.help,
|
help_keymap = state.settings.help,
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.settings.discussion_tree.winbar(t)
|
return M.make_winbar(t)
|
||||||
end
|
end
|
||||||
|
|
||||||
---This function updates the winbar
|
---This function updates the winbar
|
||||||
---@param discussions Discussion[]
|
M.update_winbar = function()
|
||||||
---@param unlinked_discussions UnlinkedDiscussion[]
|
|
||||||
---@param base_title string
|
|
||||||
M.update_winbar = function(discussions, unlinked_discussions, base_title)
|
|
||||||
local d = require("gitlab.actions.discussions")
|
local d = require("gitlab.actions.discussions")
|
||||||
local winId = d.split.winid
|
if d.split == nil then
|
||||||
local c = content(discussions, unlinked_discussions, base_title)
|
return
|
||||||
if vim.wo[winId] then
|
end
|
||||||
vim.wo[winId].winbar = c
|
|
||||||
|
local win_id = d.split.winid
|
||||||
|
if win_id == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not vim.api.nvim_win_is_valid(win_id) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local c = content()
|
||||||
|
vim.api.nvim_set_option_value("winbar", c, { scope = "local", win = win_id })
|
||||||
|
end
|
||||||
|
|
||||||
|
---Builds the title string for both sections, using the count of resolvable and draft nodes
|
||||||
|
---@param base_title string
|
||||||
|
---@param resolvable_count integer
|
||||||
|
---@param resolved_count integer
|
||||||
|
---@param drafts_count integer
|
||||||
|
---@return string
|
||||||
|
local add_drafts_and_resolvable = function(base_title, resolvable_count, resolved_count, drafts_count)
|
||||||
|
if resolvable_count ~= 0 then
|
||||||
|
base_title = base_title .. string.format(" (%d/%d resolved", resolvable_count, resolved_count)
|
||||||
|
end
|
||||||
|
if drafts_count ~= 0 then
|
||||||
|
if resolvable_count ~= 0 then
|
||||||
|
base_title = base_title .. string.format("; %d drafts)", drafts_count)
|
||||||
|
else
|
||||||
|
base_title = base_title .. string.format(" (%d drafts)", drafts_count)
|
||||||
|
end
|
||||||
|
elseif resolvable_count ~= 0 then
|
||||||
|
base_title = base_title .. ")"
|
||||||
|
end
|
||||||
|
|
||||||
|
return base_title
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param t WinbarTable
|
||||||
|
M.make_winbar = function(t)
|
||||||
|
local discussion_title =
|
||||||
|
add_drafts_and_resolvable("Inline Comments", t.resolvable_discussions, t.resolved_discussions, t.inline_draft_notes)
|
||||||
|
local notes_title = add_drafts_and_resolvable("Notes", t.resolvable_notes, t.resolved_notes, t.unlinked_draft_notes)
|
||||||
|
|
||||||
|
-- Colorize the active tab
|
||||||
|
if M.current_view_type == "discussions" then
|
||||||
|
discussion_title = "%#Text#" .. discussion_title
|
||||||
|
notes_title = "%#Comment#" .. notes_title
|
||||||
|
elseif M.current_view_type == "notes" then
|
||||||
|
discussion_title = "%#Comment#" .. discussion_title
|
||||||
|
notes_title = "%#Text#" .. notes_title
|
||||||
|
end
|
||||||
|
|
||||||
|
local mode = M.get_mode()
|
||||||
|
|
||||||
|
-- Join everything together and return it
|
||||||
|
local separator = "%#Comment#|"
|
||||||
|
local end_section = "%="
|
||||||
|
local help = "%#Comment#Help: " .. t.help_keymap:gsub(" ", "<space>") .. " "
|
||||||
|
return string.format(
|
||||||
|
" %s %s %s %s %s %s %s",
|
||||||
|
discussion_title,
|
||||||
|
separator,
|
||||||
|
notes_title,
|
||||||
|
end_section,
|
||||||
|
mode,
|
||||||
|
separator,
|
||||||
|
help
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns a string for the winbar indicating the mode type, live or draft
|
||||||
|
---@return string
|
||||||
|
M.get_mode = function()
|
||||||
|
if state.settings.discussion_tree.draft_mode then
|
||||||
|
return "%#DiagnosticWarn#Draft Mode"
|
||||||
|
else
|
||||||
|
return "%#DiagnosticOK#Live Mode"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Sets the current view type (if provided an argument)
|
||||||
|
---and then updates the view
|
||||||
|
---@param override any
|
||||||
|
M.switch_view_type = function(override)
|
||||||
|
if override then
|
||||||
|
M.current_view_type = override
|
||||||
|
else
|
||||||
|
if M.current_view_type == "discussions" then
|
||||||
|
M.current_view_type = "notes"
|
||||||
|
elseif M.current_view_type == "notes" then
|
||||||
|
M.current_view_type = "discussions"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_buf(M.bufnr_map[M.current_view_type])
|
||||||
|
M.update_winbar()
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
239
lua/gitlab/actions/draft_notes/init.lua
Executable file
239
lua/gitlab/actions/draft_notes/init.lua
Executable file
@@ -0,0 +1,239 @@
|
|||||||
|
-- This module is responsible for CRUD operations for the draft notes in the discussion tree.
|
||||||
|
-- That includes things like editing existing draft notes in the tree, and
|
||||||
|
-- and deleting them. Normal notes and comments are managed separately,
|
||||||
|
-- under lua/gitlab/actions/discussions/init.lua
|
||||||
|
local winbar = require("gitlab.actions.discussions.winbar")
|
||||||
|
local diagnostics = require("gitlab.indicators.diagnostics")
|
||||||
|
local common = require("gitlab.actions.common")
|
||||||
|
local discussion_tree = require("gitlab.actions.discussions.tree")
|
||||||
|
local job = require("gitlab.job")
|
||||||
|
local NuiTree = require("nui.tree")
|
||||||
|
local List = require("gitlab.utils.list")
|
||||||
|
local u = require("gitlab.utils")
|
||||||
|
local state = require("gitlab.state")
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---@class AddDraftNoteOpts table
|
||||||
|
---@field draft_note DraftNote
|
||||||
|
---@field unlinked boolean
|
||||||
|
|
||||||
|
---Adds a draft note to the draft notes state, then rebuilds the view
|
||||||
|
---@param opts AddDraftNoteOpts
|
||||||
|
M.add_draft_note = function(opts)
|
||||||
|
local new_draft_notes = u.ensure_table(state.DRAFT_NOTES)
|
||||||
|
table.insert(new_draft_notes, opts.draft_note)
|
||||||
|
state.DRAFT_NOTES = new_draft_notes
|
||||||
|
local discussions = require("gitlab.actions.discussions")
|
||||||
|
if opts.unlinked then
|
||||||
|
discussions.rebuild_unlinked_discussion_tree()
|
||||||
|
else
|
||||||
|
discussions.rebuild_discussion_tree()
|
||||||
|
end
|
||||||
|
winbar.update_winbar()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Tells whether a draft note was left on a particular diff or is an unlinked note
|
||||||
|
---@param note DraftNote
|
||||||
|
M.has_position = function(note)
|
||||||
|
return note.position.new_path ~= nil or note.position.old_path ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns a list of nodes to add to the discussion tree. Can filter and return only unlinked (note) nodes.
|
||||||
|
---@param unlinked boolean
|
||||||
|
---@return NuiTree.Node[]
|
||||||
|
M.add_draft_notes_to_table = function(unlinked)
|
||||||
|
local draft_notes = List.new(state.DRAFT_NOTES)
|
||||||
|
|
||||||
|
local draft_note_nodes = draft_notes
|
||||||
|
---@param note DraftNote
|
||||||
|
:filter(function(note)
|
||||||
|
if unlinked then
|
||||||
|
return not M.has_position(note)
|
||||||
|
end
|
||||||
|
return M.has_position(note)
|
||||||
|
end)
|
||||||
|
---@param note DraftNote
|
||||||
|
:map(function(note)
|
||||||
|
local _, root_text, root_text_nodes = discussion_tree.build_note(note)
|
||||||
|
return NuiTree.Node({
|
||||||
|
range = (type(note.position) == "table" and note.position.line_range or nil),
|
||||||
|
text = root_text,
|
||||||
|
type = "note",
|
||||||
|
is_root = true,
|
||||||
|
is_draft = true,
|
||||||
|
id = note.id,
|
||||||
|
root_note_id = note.id,
|
||||||
|
file_name = (type(note.position) == "table" and note.position.new_path or nil),
|
||||||
|
new_line = (type(note.position) == "table" and note.position.new_line or nil),
|
||||||
|
old_line = (type(note.position) == "table" and note.position.old_line or nil),
|
||||||
|
resolvable = false,
|
||||||
|
resolved = false,
|
||||||
|
url = state.INFO.web_url .. "#note_" .. note.id,
|
||||||
|
}, root_text_nodes)
|
||||||
|
end)
|
||||||
|
|
||||||
|
return draft_note_nodes
|
||||||
|
|
||||||
|
-- TODO: Combine draft_notes and normal discussion nodes in the complex discussion
|
||||||
|
-- tree. The code for that feature is a clusterfuck so this is difficult
|
||||||
|
-- if state.settings.discussion_tree.tree_type == "simple" then
|
||||||
|
-- return draft_note_nodes
|
||||||
|
-- end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Send edits will actually send the edits to Gitlab and refresh the draft_notes tree
|
||||||
|
M.send_edits = function(note_id)
|
||||||
|
return function(text)
|
||||||
|
local all_notes = List.new(state.DRAFT_NOTES)
|
||||||
|
local the_note = all_notes:find(function(note)
|
||||||
|
return note.id == note_id
|
||||||
|
end)
|
||||||
|
local body = { note = text, position = the_note.position }
|
||||||
|
job.run_job(string.format("/mr/draft_notes/%d", note_id), "PATCH", body, function(data)
|
||||||
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
|
local has_position = false
|
||||||
|
local new_draft_notes = all_notes:map(function(note)
|
||||||
|
if note.id == note_id then
|
||||||
|
has_position = M.has_position(note)
|
||||||
|
note.note = text
|
||||||
|
end
|
||||||
|
return note
|
||||||
|
end)
|
||||||
|
state.DRAFT_NOTES = new_draft_notes
|
||||||
|
local discussions = require("gitlab.actions.discussions")
|
||||||
|
if has_position then
|
||||||
|
discussions.rebuild_discussion_tree()
|
||||||
|
else
|
||||||
|
discussions.rebuild_unlinked_discussion_tree()
|
||||||
|
end
|
||||||
|
winbar.update_winbar()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This function will actually send the deletion to Gitlab when you make a selection, and re-render the tree
|
||||||
|
M.send_deletion = function(tree)
|
||||||
|
local current_node = tree:get_node()
|
||||||
|
local note_node = common.get_note_node(tree, current_node)
|
||||||
|
local root_node = common.get_root_node(tree, current_node)
|
||||||
|
|
||||||
|
if note_node == nil or root_node == nil then
|
||||||
|
u.notify("Could not get note or root node", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type integer
|
||||||
|
local note_id = note_node.is_root and root_node.id or note_node.id
|
||||||
|
|
||||||
|
job.run_job(string.format("/mr/draft_notes/%d", note_id), "DELETE", nil, function(data)
|
||||||
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
|
|
||||||
|
local has_position = false
|
||||||
|
local new_draft_notes = List.new(state.DRAFT_NOTES):filter(function(note)
|
||||||
|
if note.id ~= note_id then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
has_position = M.has_position(note)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
state.DRAFT_NOTES = new_draft_notes
|
||||||
|
local discussions = require("gitlab.actions.discussions")
|
||||||
|
if has_position then
|
||||||
|
discussions.rebuild_discussion_tree()
|
||||||
|
else
|
||||||
|
discussions.rebuild_unlinked_discussion_tree()
|
||||||
|
end
|
||||||
|
|
||||||
|
if state.settings.discussion_signs.enabled and state.DISCUSSION_DATA then
|
||||||
|
diagnostics.refresh_diagnostics()
|
||||||
|
end
|
||||||
|
|
||||||
|
winbar.update_winbar()
|
||||||
|
common.add_empty_titles()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This function will trigger a popup prompting you to publish the current draft comment
|
||||||
|
M.publish_draft = function(tree)
|
||||||
|
vim.ui.select({ "Confirm", "Cancel" }, {
|
||||||
|
prompt = "Publish current draft comment?",
|
||||||
|
}, function(choice)
|
||||||
|
if choice == "Confirm" then
|
||||||
|
M.confirm_publish_draft(tree)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This function will trigger a popup prompting you to publish all draft notes
|
||||||
|
M.publish_all_drafts = function()
|
||||||
|
vim.ui.select({ "Confirm", "Cancel" }, {
|
||||||
|
prompt = "Publish all drafts?",
|
||||||
|
}, function(choice)
|
||||||
|
if choice == "Confirm" then
|
||||||
|
M.confirm_publish_all_drafts()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Publishes all draft notes and comments. Re-renders all discussion views.
|
||||||
|
M.confirm_publish_all_drafts = function()
|
||||||
|
local body = { publish_all = true }
|
||||||
|
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
||||||
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
|
state.DRAFT_NOTES = {}
|
||||||
|
local discussions = require("gitlab.actions.discussions")
|
||||||
|
discussions.refresh(function()
|
||||||
|
discussions.rebuild_discussion_tree()
|
||||||
|
discussions.rebuild_unlinked_discussion_tree()
|
||||||
|
winbar.update_winbar()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Publishes the current draft note that is being hovered over in the tree,
|
||||||
|
---and then makes an API call to refresh the relevant data for that tree
|
||||||
|
---and re-render it.
|
||||||
|
---@param tree NuiTree
|
||||||
|
M.confirm_publish_draft = function(tree)
|
||||||
|
local current_node = tree:get_node()
|
||||||
|
local note_node = common.get_note_node(tree, current_node)
|
||||||
|
local root_node = common.get_root_node(tree, current_node)
|
||||||
|
|
||||||
|
if note_node == nil or root_node == nil then
|
||||||
|
u.notify("Could not get note or root node", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type integer
|
||||||
|
local note_id = note_node.is_root and root_node.id or note_node.id
|
||||||
|
local body = { note = note_id, publish_all = false }
|
||||||
|
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
||||||
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
|
|
||||||
|
local has_position = false
|
||||||
|
local new_draft_notes = List.new(state.DRAFT_NOTES):filter(function(note)
|
||||||
|
if note.id ~= note_id then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
has_position = M.has_position(note)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
state.DRAFT_NOTES = new_draft_notes
|
||||||
|
local discussions = require("gitlab.actions.discussions")
|
||||||
|
discussions.refresh(function()
|
||||||
|
if has_position then
|
||||||
|
discussions.rebuild_discussion_tree()
|
||||||
|
else
|
||||||
|
discussions.rebuild_unlinked_discussion_tree()
|
||||||
|
end
|
||||||
|
winbar.update_winbar()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
56
lua/gitlab/actions/merge_requests.lua
Normal file
56
lua/gitlab/actions/merge_requests.lua
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
local state = require("gitlab.state")
|
||||||
|
local reviewer = require("gitlab.reviewer")
|
||||||
|
local git = require("gitlab.git")
|
||||||
|
local u = require("gitlab.utils")
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---@class SwitchOpts
|
||||||
|
---@field open_reviewer boolean
|
||||||
|
|
||||||
|
---Opens up a select menu that lets you choose a different merge request.
|
||||||
|
---@param opts SwitchOpts|nil
|
||||||
|
M.choose_merge_request = function(opts)
|
||||||
|
local has_clean_tree, clean_tree_err = git.has_clean_tree()
|
||||||
|
if clean_tree_err ~= nil then
|
||||||
|
return
|
||||||
|
elseif has_clean_tree ~= "" then
|
||||||
|
u.notify("Your local branch has changes, please stash or commit and push", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts == nil then
|
||||||
|
opts = state.settings.choose_merge_request
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.ui.select(state.MERGE_REQUESTS, {
|
||||||
|
prompt = "Choose Merge Request",
|
||||||
|
format_item = function(mr)
|
||||||
|
return string.format("%s (%s)", mr.title, mr.author.name)
|
||||||
|
end,
|
||||||
|
}, function(choice)
|
||||||
|
if not choice then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if reviewer.is_open then
|
||||||
|
reviewer.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
local _, branch_switch_err = git.switch_branch(choice.source_branch)
|
||||||
|
if branch_switch_err ~= nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
require("gitlab.server").restart(function()
|
||||||
|
if opts.open_reviewer then
|
||||||
|
require("gitlab").review()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -143,10 +143,7 @@ M.see_logs = function()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local lines = {}
|
local lines = u.lines_into_table(file)
|
||||||
for line in u.split_by_new_lines(file) do
|
|
||||||
table.insert(lines, line)
|
|
||||||
end
|
|
||||||
|
|
||||||
if #lines == 0 then
|
if #lines == 0 then
|
||||||
u.notify("Log trace lines could not be parsed", vim.log.levels.ERROR)
|
u.notify("Log trace lines could not be parsed", vim.log.levels.ERROR)
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
-- send edits to the description back to Gitlab
|
-- send edits to the description back to Gitlab
|
||||||
local Layout = require("nui.layout")
|
local Layout = require("nui.layout")
|
||||||
local Popup = require("nui.popup")
|
local Popup = require("nui.popup")
|
||||||
|
local git = require("gitlab.git")
|
||||||
local job = require("gitlab.job")
|
local job = require("gitlab.job")
|
||||||
|
local common = require("gitlab.actions.common")
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local List = require("gitlab.utils.list")
|
local List = require("gitlab.utils.list")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
@@ -28,7 +30,7 @@ M.summary = function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local title = state.INFO.title
|
local title = state.INFO.title
|
||||||
local description_lines = M.build_description_lines()
|
local description_lines = common.build_content(state.INFO.description)
|
||||||
local info_lines = state.settings.info.enabled and M.build_info_lines() or { "" }
|
local info_lines = state.settings.info.enabled and M.build_info_lines() or { "" }
|
||||||
|
|
||||||
local layout, title_popup, description_popup, info_popup = M.create_layout(info_lines)
|
local layout, title_popup, description_popup, info_popup = M.create_layout(info_lines)
|
||||||
@@ -69,22 +71,8 @@ M.summary = function()
|
|||||||
|
|
||||||
vim.api.nvim_set_current_buf(description_popup.bufnr)
|
vim.api.nvim_set_current_buf(description_popup.bufnr)
|
||||||
end)
|
end)
|
||||||
end
|
|
||||||
|
|
||||||
-- Builds a lua list of strings that contain the MR description
|
git.current_branch_up_to_date_on_remote(vim.log.levels.WARN)
|
||||||
M.build_description_lines = function()
|
|
||||||
local description_lines = {}
|
|
||||||
|
|
||||||
local description = state.INFO.description
|
|
||||||
for line in u.split_by_new_lines(description) do
|
|
||||||
table.insert(description_lines, line)
|
|
||||||
end
|
|
||||||
-- TODO: @harrisoncramer Not sure whether the following line should be here at all. It definitely
|
|
||||||
-- didn't belong into the for loop, since it inserted an empty line after each line. But maybe
|
|
||||||
-- there is a purpose for an empty line at the end of the buffer?
|
|
||||||
table.insert(description_lines, "")
|
|
||||||
|
|
||||||
return description_lines
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Builds a lua list of strings that contain metadata about the current MR. Only builds the
|
-- Builds a lua list of strings that contain metadata about the current MR. Only builds the
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ function async:fetch(dependencies, i, argTable)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Call the API, set the data, and then call the next API
|
-- Call the API, set the data, and then call the next API
|
||||||
job.run_job(dependency.endpoint, "GET", dependency.body, function(data)
|
local body = dependency.body and dependency.body() or nil
|
||||||
state[dependency.state] = data[dependency.key]
|
job.run_job(dependency.endpoint, dependency.method or "GET", body, function(data)
|
||||||
|
state[dependency.state] = dependency.key and data[dependency.key] or data
|
||||||
self:fetch(dependencies, i + 1, argTable)
|
self:fetch(dependencies, i + 1, argTable)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ vim.api.nvim_set_hl(0, "GitlabDirectoryIcon", u.get_colors_for_group(discussion.
|
|||||||
vim.api.nvim_set_hl(0, "GitlabFileName", u.get_colors_for_group(discussion.file_name))
|
vim.api.nvim_set_hl(0, "GitlabFileName", u.get_colors_for_group(discussion.file_name))
|
||||||
vim.api.nvim_set_hl(0, "GitlabResolved", u.get_colors_for_group(discussion.resolved))
|
vim.api.nvim_set_hl(0, "GitlabResolved", u.get_colors_for_group(discussion.resolved))
|
||||||
vim.api.nvim_set_hl(0, "GitlabUnresolved", u.get_colors_for_group(discussion.unresolved))
|
vim.api.nvim_set_hl(0, "GitlabUnresolved", u.get_colors_for_group(discussion.unresolved))
|
||||||
|
vim.api.nvim_set_hl(0, "GitlabDraft", u.get_colors_for_group(discussion.draft))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
|
local common = require("gitlab.actions.common")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
|
|
||||||
local M = {
|
local M = {
|
||||||
@@ -70,15 +71,15 @@ M.init_popup = function(tree, bufnr)
|
|||||||
vim.api.nvim_create_autocmd({ "CursorHold" }, {
|
vim.api.nvim_create_autocmd({ "CursorHold" }, {
|
||||||
callback = function()
|
callback = function()
|
||||||
local node = tree:get_node()
|
local node = tree:get_node()
|
||||||
if node == nil or not require("gitlab.actions.discussions").is_node_note(node) then
|
if node == nil or not common.is_node_note(node) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local note_node = require("gitlab.actions.discussions").get_note_node(tree, node)
|
local note_node = common.get_note_node(tree, node)
|
||||||
local root_node = require("gitlab.actions.discussions").get_root_node(tree, node)
|
local root_node = common.get_root_node(tree, node)
|
||||||
local note_id_str = tostring(note_node.is_root and root_node.root_note_id or note_node.id)
|
local note_id_str = tostring(note_node.is_root and root_node.root_note_id or note_node.id)
|
||||||
|
local emojis = state.DISCUSSION_DATA.emojis
|
||||||
|
|
||||||
local emojis = require("gitlab.actions.discussions").emojis
|
|
||||||
local note_emojis = emojis[note_id_str]
|
local note_emojis = emojis[note_id_str]
|
||||||
if note_emojis == nil then
|
if note_emojis == nil then
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,11 +1,122 @@
|
|||||||
|
local List = require("gitlab.utils.list")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
M.has_clean_tree = function()
|
---Runs a system command, captures the output (if it exists) and handles errors
|
||||||
return vim.fn.trim(vim.fn.system({ "git", "status", "--short", "--untracked-files=no" })) == ""
|
---@param command table
|
||||||
|
---@return string|nil, string|nil
|
||||||
|
local run_system = function(command)
|
||||||
|
local u = require("gitlab.utils")
|
||||||
|
local result = vim.fn.trim(vim.fn.system(command))
|
||||||
|
if vim.v.shell_error ~= 0 then
|
||||||
|
u.notify(result, vim.log.levels.ERROR)
|
||||||
|
return nil, result
|
||||||
|
end
|
||||||
|
return result, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Returns all branches for the current repository
|
||||||
|
---@return string|nil, string|nil
|
||||||
|
M.branches = function()
|
||||||
|
return run_system({ "git", "branch" })
|
||||||
|
end
|
||||||
|
|
||||||
|
---Checks whether the tree has any changes that haven't been pushed to the remote
|
||||||
|
---@return string|nil, string|nil
|
||||||
|
M.has_clean_tree = function()
|
||||||
|
return run_system({ "git", "status", "--short", "--untracked-files=no" })
|
||||||
|
end
|
||||||
|
|
||||||
|
---Gets the base directory of the current project
|
||||||
|
---@return string|nil, string|nil
|
||||||
M.base_dir = function()
|
M.base_dir = function()
|
||||||
return vim.fn.trim(vim.fn.system({ "git", "rev-parse", "--show-toplevel" }))
|
return run_system({ "git", "rev-parse", "--show-toplevel" })
|
||||||
|
end
|
||||||
|
|
||||||
|
---Switches the current project to the given branch
|
||||||
|
---@return string|nil, string|nil
|
||||||
|
M.switch_branch = function(branch)
|
||||||
|
return run_system({ "git", "checkout", "-q", branch })
|
||||||
|
end
|
||||||
|
|
||||||
|
---Return the name of the current branch
|
||||||
|
---@return string|nil, string|nil
|
||||||
|
M.get_current_branch = function()
|
||||||
|
return run_system({ "git", "branch", "--show-current" })
|
||||||
|
end
|
||||||
|
|
||||||
|
---Return the list of possible merge targets.
|
||||||
|
---@return table|nil
|
||||||
|
M.get_all_merge_targets = function()
|
||||||
|
local current_branch, err = M.get_current_branch()
|
||||||
|
if not current_branch or err ~= nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
return List.new(M.get_all_remote_branches()):filter(function(branch)
|
||||||
|
return branch ~= current_branch
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Return the list of names of all remote-tracking branches or an empty list.
|
||||||
|
---@return table, string|nil
|
||||||
|
M.get_all_remote_branches = function()
|
||||||
|
local all_branches, err = M.branches()
|
||||||
|
if err ~= nil then
|
||||||
|
return {}, err
|
||||||
|
end
|
||||||
|
if all_branches == nil then
|
||||||
|
return {}, "Something went wrong getting branches for this repository"
|
||||||
|
end
|
||||||
|
|
||||||
|
local u = require("gitlab.utils")
|
||||||
|
local lines = u.lines_into_table(all_branches)
|
||||||
|
return List.new(lines)
|
||||||
|
:map(function(line)
|
||||||
|
-- Trim "origin/"
|
||||||
|
return line:match("origin/(%S+)")
|
||||||
|
end)
|
||||||
|
:filter(function(branch)
|
||||||
|
-- Don't include the HEAD pointer
|
||||||
|
return not branch:match("^HEAD$")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Return whether something
|
||||||
|
---@param current_branch string
|
||||||
|
---@return string|nil, string|nil
|
||||||
|
M.contains_branch = function(current_branch)
|
||||||
|
return run_system({ "git", "branch", "-r", "--contains", current_branch })
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns true if `branch` is up-to-date on remote, false otherwise.
|
||||||
|
---@param log_level integer
|
||||||
|
---@return boolean|nil
|
||||||
|
M.current_branch_up_to_date_on_remote = function(log_level)
|
||||||
|
local current_branch = M.get_current_branch()
|
||||||
|
local handle = io.popen("git branch -r --contains " .. current_branch .. " 2>&1")
|
||||||
|
if not handle then
|
||||||
|
require("gitlab.utils").notify("Error running 'git branch' command.", vim.log.levels.ERROR)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local remote_branches_with_current_head = {}
|
||||||
|
for line in handle:lines() do
|
||||||
|
table.insert(remote_branches_with_current_head, line)
|
||||||
|
end
|
||||||
|
handle:close()
|
||||||
|
|
||||||
|
local current_head_on_remote = List.new(remote_branches_with_current_head):filter(function(line)
|
||||||
|
return line == " origin/" .. current_branch
|
||||||
|
end)
|
||||||
|
local remote_up_to_date = #current_head_on_remote == 1
|
||||||
|
|
||||||
|
if not remote_up_to_date then
|
||||||
|
require("gitlab.utils").notify(
|
||||||
|
"You have local commits that are not on origin. Have you forgotten to push?",
|
||||||
|
log_level
|
||||||
|
)
|
||||||
|
end
|
||||||
|
return remote_up_to_date
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -5,30 +5,57 @@ local List = require("gitlab.utils.list")
|
|||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
---@class NoteWithValues
|
||||||
|
---@field position NotePosition
|
||||||
|
---@field resolvable boolean|nil
|
||||||
|
---@field resolved boolean|nil
|
||||||
|
---@field created_at string|nil
|
||||||
|
|
||||||
|
---@param note NoteWithValues
|
||||||
|
---@param file string
|
||||||
|
---@return boolean
|
||||||
|
local filter_discussions_and_notes = function(note, file)
|
||||||
|
---Do not include unlinked notes
|
||||||
|
return note.position ~= nil
|
||||||
|
and (note.position.new_path == file or note.position.old_path == file)
|
||||||
|
---Skip resolved discussions if user wants to
|
||||||
|
and not (state.settings.discussion_signs.skip_resolved_discussion and note.resolvable and note.resolved)
|
||||||
|
---Skip discussions from old revisions
|
||||||
|
and not (
|
||||||
|
state.settings.discussion_signs.skip_old_revision_discussion
|
||||||
|
and u.from_iso_format_date_to_timestamp(note.created_at)
|
||||||
|
<= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
---Filter all discussions which are relevant for currently visible signs and diagnostics.
|
---Filter all discussions which are relevant for currently visible signs and diagnostics.
|
||||||
---@return Discussion[]
|
---@return Discussion|DraftNote[]
|
||||||
M.filter_placeable_discussions = function(all_discussions)
|
M.filter_placeable_discussions = function()
|
||||||
if type(all_discussions) ~= "table" then
|
local discussions = u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions or {})
|
||||||
return {}
|
if type(discussions) ~= "table" then
|
||||||
|
discussions = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local draft_notes = u.ensure_table(state.DRAFT_NOTES)
|
||||||
|
if type(draft_notes) ~= "table" then
|
||||||
|
draft_notes = {}
|
||||||
|
end
|
||||||
|
|
||||||
local file = reviewer.get_current_file()
|
local file = reviewer.get_current_file()
|
||||||
if not file then
|
if not file then
|
||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
return List.new(all_discussions):filter(function(discussion)
|
|
||||||
|
local filtered_discussions = List.new(discussions):filter(function(discussion)
|
||||||
local first_note = discussion.notes[1]
|
local first_note = discussion.notes[1]
|
||||||
return type(first_note.position) == "table"
|
return type(first_note.position) == "table" and filter_discussions_and_notes(first_note, file)
|
||||||
--Do not include unlinked notes
|
|
||||||
and (first_note.position.new_path == file or first_note.position.old_path == file)
|
|
||||||
--Skip resolved discussions if user wants to
|
|
||||||
and not (state.settings.discussion_signs.skip_resolved_discussion and first_note.resolvable and first_note.resolved)
|
|
||||||
--Skip discussions from old revisions
|
|
||||||
and not (
|
|
||||||
state.settings.discussion_signs.skip_old_revision_discussion
|
|
||||||
and u.from_iso_format_date_to_timestamp(first_note.created_at)
|
|
||||||
<= u.from_iso_format_date_to_timestamp(state.MR_REVISIONS[1].created_at)
|
|
||||||
)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local filtered_draft_notes = List.new(draft_notes):filter(function(note)
|
||||||
|
return filter_discussions_and_notes(note, file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
return u.join(filtered_discussions, filtered_draft_notes)
|
||||||
end
|
end
|
||||||
|
|
||||||
M.parse_line_code = function(line_code)
|
M.parse_line_code = function(line_code)
|
||||||
@@ -37,24 +64,24 @@ M.parse_line_code = function(line_code)
|
|||||||
return tonumber(old_line), tonumber(new_line)
|
return tonumber(old_line), tonumber(new_line)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param discussion Discussion
|
---@param d_or_n Discussion|DraftNote
|
||||||
---@return boolean
|
---@return boolean
|
||||||
M.is_old_sha = function(discussion)
|
M.is_old_sha = function(d_or_n)
|
||||||
local first_note = discussion.notes[1]
|
local first_note = M.get_first_note(d_or_n)
|
||||||
return first_note.position.old_line ~= nil
|
return first_note.position.old_line ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param discussion Discussion
|
---@param discussion Discussion|DraftNote
|
||||||
---@return boolean
|
---@return boolean
|
||||||
M.is_new_sha = function(discussion)
|
M.is_new_sha = function(discussion)
|
||||||
return not M.is_old_sha(discussion)
|
return not M.is_old_sha(discussion)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param discussion Discussion
|
---@param d_or_n Discussion|DraftNote
|
||||||
---@return boolean
|
---@return boolean
|
||||||
M.is_single_line = function(discussion)
|
M.is_single_line = function(d_or_n)
|
||||||
local first_note = discussion.notes[1]
|
local first_note = M.get_first_note(d_or_n)
|
||||||
local line_range = first_note.position.line_range
|
local line_range = first_note.position and first_note.position.line_range
|
||||||
return line_range == nil
|
return line_range == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,10 +91,10 @@ M.is_multi_line = function(discussion)
|
|||||||
return not M.is_single_line(discussion)
|
return not M.is_single_line(discussion)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param discussion Discussion
|
---@param d_or_n Discussion|DraftNote
|
||||||
---@return Note
|
---@return Note|DraftNote
|
||||||
M.get_first_note = function(discussion)
|
M.get_first_note = function(d_or_n)
|
||||||
return discussion.notes[1]
|
return d_or_n.notes and d_or_n.notes[1] or d_or_n
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local diffview_lib = require("diffview.lib")
|
local diffview_lib = require("diffview.lib")
|
||||||
local discussion_tree = require("gitlab.actions.discussions.tree")
|
local indicators_common = require("gitlab.indicators.common")
|
||||||
local common = require("gitlab.indicators.common")
|
local actions_common = require("gitlab.actions.common")
|
||||||
local List = require("gitlab.utils.list")
|
local List = require("gitlab.utils.list")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local discussion_sign_name = "gitlab_discussion"
|
local discussion_sign_name = "gitlab_discussion"
|
||||||
@@ -24,19 +24,23 @@ local display_opts = {
|
|||||||
---Takes some range information and data about a discussion
|
---Takes some range information and data about a discussion
|
||||||
---and creates a diagnostic to be placed in the reviewer
|
---and creates a diagnostic to be placed in the reviewer
|
||||||
---@param range_info table
|
---@param range_info table
|
||||||
---@param discussion Discussion
|
---@param d_or_n Discussion|DraftNote
|
||||||
---@return Diagnostic
|
---@return Diagnostic
|
||||||
local function create_diagnostic(range_info, discussion)
|
local function create_diagnostic(range_info, d_or_n)
|
||||||
local message = ""
|
local first_note = indicators_common.get_first_note(d_or_n)
|
||||||
for _, note in ipairs(discussion.notes) do
|
local header = actions_common.build_note_header(first_note)
|
||||||
message = message .. discussion_tree.build_note_header(note) .. "\n" .. note.body .. "\n"
|
local message = header
|
||||||
|
if d_or_n.notes then
|
||||||
|
for _, note in ipairs(d_or_n.notes or {}) do
|
||||||
|
message = message .. actions_common.build_note_header(note) .. "\n" .. note.body .. "\n"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local diagnostic = {
|
local diagnostic = {
|
||||||
message = message,
|
message = message,
|
||||||
col = 0,
|
col = 0,
|
||||||
severity = state.settings.discussion_signs.severity,
|
severity = state.settings.discussion_signs.severity,
|
||||||
user_data = { discussion_id = discussion.id, header = discussion_tree.build_note_header(discussion.notes[1]) },
|
user_data = { discussion_id = d_or_n.id, header = header },
|
||||||
source = "gitlab",
|
source = "gitlab",
|
||||||
code = "gitlab.nvim",
|
code = "gitlab.nvim",
|
||||||
}
|
}
|
||||||
@@ -44,38 +48,37 @@ local function create_diagnostic(range_info, discussion)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---Creates a single line diagnostic
|
---Creates a single line diagnostic
|
||||||
---@param discussion Discussion
|
---@param d_or_n Discussion|DraftNote
|
||||||
---@return Diagnostic
|
---@return Diagnostic
|
||||||
local create_single_line_diagnostic = function(discussion)
|
local create_single_line_diagnostic = function(d_or_n)
|
||||||
local first_note = discussion.notes[1]
|
local linnr = actions_common.get_line_number(d_or_n.id)
|
||||||
local linnr = (common.is_new_sha(discussion) and first_note.position.new_line or first_note.position.old_line) or 1
|
|
||||||
return create_diagnostic({
|
return create_diagnostic({
|
||||||
lnum = linnr - 1,
|
lnum = linnr - 1,
|
||||||
}, discussion)
|
}, d_or_n)
|
||||||
end
|
end
|
||||||
|
|
||||||
---Creates a mutli-line line diagnostic
|
---Creates a mutli-line line diagnostic
|
||||||
---@param discussion Discussion
|
---@param d_or_n Discussion|DraftNote
|
||||||
---@return Diagnostic
|
---@return Diagnostic
|
||||||
local create_multiline_diagnostic = function(discussion)
|
local create_multiline_diagnostic = function(d_or_n)
|
||||||
local first_note = discussion.notes[1]
|
local first_note = indicators_common.get_first_note(d_or_n)
|
||||||
local line_range = first_note.position.line_range
|
local line_range = first_note.position.line_range
|
||||||
if line_range == nil then
|
if line_range == nil then
|
||||||
error("Parsing multi-line comment but note does not contain line range")
|
error("Parsing multi-line comment but note does not contain line range")
|
||||||
end
|
end
|
||||||
|
|
||||||
local start_old_line, start_new_line = common.parse_line_code(line_range.start.line_code)
|
local start_old_line, start_new_line = indicators_common.parse_line_code(line_range.start.line_code)
|
||||||
|
|
||||||
if common.is_new_sha(discussion) then
|
if indicators_common.is_new_sha(d_or_n) then
|
||||||
return create_diagnostic({
|
return create_diagnostic({
|
||||||
lnum = start_new_line - 1,
|
lnum = start_new_line - 1,
|
||||||
end_lnum = first_note.position.new_line - 1,
|
end_lnum = first_note.position.new_line - 1,
|
||||||
}, discussion)
|
}, d_or_n)
|
||||||
else
|
else
|
||||||
return create_diagnostic({
|
return create_diagnostic({
|
||||||
lnum = start_old_line - 1,
|
lnum = start_old_line - 1,
|
||||||
end_lnum = first_note.position.old_line - 1,
|
end_lnum = first_note.position.old_line - 1,
|
||||||
}, discussion)
|
}, d_or_n)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -106,12 +109,11 @@ local set_diagnostics_in_old_sha = function(namespace, diagnostics, opts)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---Refresh the diagnostics for the currently reviewed file
|
---Refresh the diagnostics for the currently reviewed file
|
||||||
---@param discussions Discussion[]
|
M.refresh_diagnostics = function()
|
||||||
M.refresh_diagnostics = function(discussions)
|
|
||||||
local ok, err = pcall(function()
|
local ok, err = pcall(function()
|
||||||
require("gitlab.indicators.signs").clear_signs()
|
require("gitlab.indicators.signs").clear_signs()
|
||||||
M.clear_diagnostics()
|
M.clear_diagnostics()
|
||||||
local filtered_discussions = common.filter_placeable_discussions(discussions)
|
local filtered_discussions = indicators_common.filter_placeable_discussions()
|
||||||
if filtered_discussions == nil then
|
if filtered_discussions == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -133,9 +135,9 @@ end
|
|||||||
---@param discussions Discussion[]
|
---@param discussions Discussion[]
|
||||||
---@return DiagnosticTable[]
|
---@return DiagnosticTable[]
|
||||||
M.parse_new_diagnostics = function(discussions)
|
M.parse_new_diagnostics = function(discussions)
|
||||||
local new_diagnostics = List.new(discussions):filter(common.is_new_sha)
|
local new_diagnostics = List.new(discussions):filter(indicators_common.is_new_sha)
|
||||||
local single_line = new_diagnostics:filter(common.is_single_line):map(create_single_line_diagnostic)
|
local single_line = new_diagnostics:filter(indicators_common.is_single_line):map(create_single_line_diagnostic)
|
||||||
local multi_line = new_diagnostics:filter(common.is_multi_line):map(create_multiline_diagnostic)
|
local multi_line = new_diagnostics:filter(indicators_common.is_multi_line):map(create_multiline_diagnostic)
|
||||||
return u.combine(single_line, multi_line)
|
return u.combine(single_line, multi_line)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -144,9 +146,9 @@ end
|
|||||||
---@param discussions Discussion[]
|
---@param discussions Discussion[]
|
||||||
---@return DiagnosticTable[]
|
---@return DiagnosticTable[]
|
||||||
M.parse_old_diagnostics = function(discussions)
|
M.parse_old_diagnostics = function(discussions)
|
||||||
local old_diagnostics = List.new(discussions):filter(common.is_old_sha)
|
local old_diagnostics = List.new(discussions):filter(indicators_common.is_old_sha)
|
||||||
local single_line = old_diagnostics:filter(common.is_single_line):map(create_single_line_diagnostic)
|
local single_line = old_diagnostics:filter(indicators_common.is_single_line):map(create_single_line_diagnostic)
|
||||||
local multi_line = old_diagnostics:filter(common.is_multi_line):map(create_multiline_diagnostic)
|
local multi_line = old_diagnostics:filter(indicators_common.is_multi_line):map(create_multiline_diagnostic)
|
||||||
return u.combine(single_line, multi_line)
|
return u.combine(single_line, multi_line)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ local emoji = require("gitlab.emoji")
|
|||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local reviewer = require("gitlab.reviewer")
|
local reviewer = require("gitlab.reviewer")
|
||||||
local discussions = require("gitlab.actions.discussions")
|
local discussions = require("gitlab.actions.discussions")
|
||||||
|
local merge_requests = require("gitlab.actions.merge_requests")
|
||||||
local merge = require("gitlab.actions.merge")
|
local merge = require("gitlab.actions.merge")
|
||||||
local summary = require("gitlab.actions.summary")
|
local summary = require("gitlab.actions.summary")
|
||||||
local data = require("gitlab.actions.data")
|
local data = require("gitlab.actions.data")
|
||||||
@@ -14,6 +15,7 @@ local comment = require("gitlab.actions.comment")
|
|||||||
local pipeline = require("gitlab.actions.pipeline")
|
local pipeline = require("gitlab.actions.pipeline")
|
||||||
local create_mr = require("gitlab.actions.create_mr")
|
local create_mr = require("gitlab.actions.create_mr")
|
||||||
local approvals = require("gitlab.actions.approvals")
|
local approvals = require("gitlab.actions.approvals")
|
||||||
|
local draft_notes = require("gitlab.actions.draft_notes")
|
||||||
local labels = require("gitlab.actions.labels")
|
local labels = require("gitlab.actions.labels")
|
||||||
|
|
||||||
local user = state.dependencies.user
|
local user = state.dependencies.user
|
||||||
@@ -22,6 +24,9 @@ local labels_dep = state.dependencies.labels
|
|||||||
local project_members = state.dependencies.project_members
|
local project_members = state.dependencies.project_members
|
||||||
local latest_pipeline = state.dependencies.latest_pipeline
|
local latest_pipeline = state.dependencies.latest_pipeline
|
||||||
local revisions = state.dependencies.revisions
|
local revisions = state.dependencies.revisions
|
||||||
|
local merge_requests_dep = state.dependencies.merge_requests
|
||||||
|
local draft_notes_dep = state.dependencies.draft_notes
|
||||||
|
local discussion_data = state.dependencies.discussion_data
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setup = function(args)
|
setup = function(args)
|
||||||
@@ -63,15 +68,20 @@ return {
|
|||||||
pipeline = async.sequence({ latest_pipeline }, pipeline.open),
|
pipeline = async.sequence({ latest_pipeline }, pipeline.open),
|
||||||
merge = async.sequence({ u.merge(info, { refresh = true }) }, merge.merge),
|
merge = async.sequence({ u.merge(info, { refresh = true }) }, merge.merge),
|
||||||
-- Discussion Tree Actions 🌴
|
-- Discussion Tree Actions 🌴
|
||||||
toggle_discussions = async.sequence({ info, user }, discussions.toggle),
|
toggle_discussions = async.sequence({
|
||||||
edit_comment = async.sequence({ info }, discussions.edit_comment),
|
info,
|
||||||
delete_comment = async.sequence({ info }, discussions.delete_comment),
|
user,
|
||||||
|
draft_notes_dep,
|
||||||
|
discussion_data,
|
||||||
|
}, discussions.toggle),
|
||||||
toggle_resolved = async.sequence({ info }, discussions.toggle_discussion_resolved),
|
toggle_resolved = async.sequence({ info }, discussions.toggle_discussion_resolved),
|
||||||
|
publish_all_drafts = draft_notes.publish_all_drafts,
|
||||||
reply = async.sequence({ info }, discussions.reply),
|
reply = async.sequence({ info }, discussions.reply),
|
||||||
-- Other functions 🤷
|
-- Other functions 🤷
|
||||||
state = state,
|
state = state,
|
||||||
data = data.data,
|
data = data.data,
|
||||||
print_settings = state.print_settings,
|
print_settings = state.print_settings,
|
||||||
|
choose_merge_request = async.sequence({ merge_requests_dep }, merge_requests.choose_merge_request),
|
||||||
open_in_browser = async.sequence({ info }, function()
|
open_in_browser = async.sequence({ info }, function()
|
||||||
local web_url = u.get_web_url()
|
local web_url = u.get_web_url()
|
||||||
if web_url ~= nil then
|
if web_url ~= nil then
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ local async = require("diffview.async")
|
|||||||
local diffview_lib = require("diffview.lib")
|
local diffview_lib = require("diffview.lib")
|
||||||
|
|
||||||
local M = {
|
local M = {
|
||||||
|
is_open = false,
|
||||||
bufnr = nil,
|
bufnr = nil,
|
||||||
tabnr = nil,
|
tabnr = nil,
|
||||||
stored_win = nil,
|
stored_win = nil,
|
||||||
@@ -41,12 +42,17 @@ M.open = function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local diffview_open_command = "DiffviewOpen"
|
local diffview_open_command = "DiffviewOpen"
|
||||||
local has_clean_tree = git.has_clean_tree()
|
local has_clean_tree, err = git.has_clean_tree()
|
||||||
|
if err ~= nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
if state.settings.reviewer_settings.diffview.imply_local and has_clean_tree then
|
if state.settings.reviewer_settings.diffview.imply_local and has_clean_tree then
|
||||||
diffview_open_command = diffview_open_command .. " --imply-local"
|
diffview_open_command = diffview_open_command .. " --imply-local"
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
|
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
|
||||||
|
|
||||||
|
M.is_open = true
|
||||||
M.tabnr = vim.api.nvim_get_current_tabpage()
|
M.tabnr = vim.api.nvim_get_current_tabpage()
|
||||||
|
|
||||||
if state.settings.reviewer_settings.diffview.imply_local and not has_clean_tree then
|
if state.settings.reviewer_settings.diffview.imply_local and not has_clean_tree then
|
||||||
@@ -74,14 +80,17 @@ M.open = function()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
require("diffview.config").user_emitter:on("view_closed", function(_, ...)
|
require("diffview.config").user_emitter:on("view_closed", function(_, ...)
|
||||||
|
M.is_open = false
|
||||||
on_diffview_closed(...)
|
on_diffview_closed(...)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if state.settings.discussion_tree.auto_open then
|
if state.settings.discussion_tree.auto_open then
|
||||||
local discussions = require("gitlab.actions.discussions")
|
local discussions = require("gitlab.actions.discussions")
|
||||||
discussions.close()
|
discussions.close()
|
||||||
discussions.toggle()
|
require("gitlab").toggle_discussions() -- Fetches data and opens discussions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
git.current_branch_up_to_date_on_remote(vim.log.levels.WARN)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Closes the reviewer and cleans up
|
-- Closes the reviewer and cleans up
|
||||||
@@ -91,7 +100,7 @@ M.close = function()
|
|||||||
discussions.close()
|
discussions.close()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Jumps to the location provided in the reviewer window
|
--- Jumps to the location provided in the reviewer window
|
||||||
---@param file_name string
|
---@param file_name string
|
||||||
---@param line_number number
|
---@param line_number number
|
||||||
---@param new_buffer boolean
|
---@param new_buffer boolean
|
||||||
@@ -172,6 +181,7 @@ M.get_reviewer_data = function()
|
|||||||
local old_line = vim.api.nvim_win_get_cursor(old_win)[1]
|
local old_line = vim.api.nvim_win_get_cursor(old_win)[1]
|
||||||
|
|
||||||
local is_current_sha_focused = M.is_current_sha_focused()
|
local is_current_sha_focused = M.is_current_sha_focused()
|
||||||
|
|
||||||
local modification_type = hunks.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
|
local modification_type = hunks.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
|
||||||
if modification_type == nil then
|
if modification_type == nil then
|
||||||
u.notify("Error getting modification type", vim.log.levels.ERROR)
|
u.notify("Error getting modification type", vim.log.levels.ERROR)
|
||||||
@@ -206,9 +216,7 @@ M.is_current_sha_focused = function()
|
|||||||
local layout = view.cur_layout
|
local layout = view.cur_layout
|
||||||
local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
|
local b_win = u.get_window_id_by_buffer_id(layout.b.file.bufnr)
|
||||||
local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
|
local a_win = u.get_window_id_by_buffer_id(layout.a.file.bufnr)
|
||||||
local current_win = vim.fn.win_getid()
|
local current_win = require("gitlab.actions.comment").current_win
|
||||||
|
|
||||||
-- Handle cases where user navigates tabs in the middle of making a comment
|
|
||||||
if a_win ~= current_win and b_win ~= current_win then
|
if a_win ~= current_win and b_win ~= current_win then
|
||||||
current_win = M.stored_win
|
current_win = M.stored_win
|
||||||
M.stored_win = nil
|
M.stored_win = nil
|
||||||
@@ -220,7 +228,7 @@ end
|
|||||||
---@return string|nil
|
---@return string|nil
|
||||||
M.get_current_file = function()
|
M.get_current_file = function()
|
||||||
local view = diffview_lib.get_current_view()
|
local view = diffview_lib.get_current_view()
|
||||||
if not view then
|
if not view or not view.panel or not view.panel.cur_file then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
return view.panel.cur_file.path
|
return view.panel.cur_file.path
|
||||||
|
|||||||
@@ -3,15 +3,51 @@
|
|||||||
-- This module is also responsible for ensuring that the state of the plugin
|
-- This module is also responsible for ensuring that the state of the plugin
|
||||||
-- is valid via dependencies
|
-- is valid via dependencies
|
||||||
|
|
||||||
|
local git = require("gitlab.git")
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
M.emoji_map = nil
|
M.emoji_map = nil
|
||||||
|
|
||||||
|
---Returns a gitlab token, and a gitlab URL. Used to connect to gitlab.
|
||||||
|
---@return string|nil, string|nil, string|nil
|
||||||
|
M.default_auth_provider = function()
|
||||||
|
local base_path, err = M.settings.config_path, nil
|
||||||
|
if base_path == nil then
|
||||||
|
base_path, err = git.base_dir()
|
||||||
|
end
|
||||||
|
|
||||||
|
if err ~= nil then
|
||||||
|
return "", ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local config_file_path = base_path .. M.settings.file_separator .. ".gitlab.nvim"
|
||||||
|
local config_file_content = u.read_file(config_file_path, { remove_newlines = true })
|
||||||
|
|
||||||
|
local file_properties = {}
|
||||||
|
if config_file_content ~= nil then
|
||||||
|
local file = assert(io.open(config_file_path, "r"))
|
||||||
|
for line in file:lines() do
|
||||||
|
for key, value in string.gmatch(line, "(.-)=(.-)$") do
|
||||||
|
file_properties[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local auth_token = file_properties.auth_token or os.getenv("GITLAB_TOKEN")
|
||||||
|
local gitlab_url = file_properties.gitlab_url or os.getenv("GITLAB_URL")
|
||||||
|
|
||||||
|
return auth_token, gitlab_url, err
|
||||||
|
end
|
||||||
|
|
||||||
-- These are the default settings for the plugin
|
-- These are the default settings for the plugin
|
||||||
M.settings = {
|
M.settings = {
|
||||||
|
auth_provider = M.default_auth_provider,
|
||||||
port = nil, -- choose random port
|
port = nil, -- choose random port
|
||||||
debug = { go_request = false, go_response = false },
|
debug = {
|
||||||
|
go_request = false,
|
||||||
|
go_response = false,
|
||||||
|
},
|
||||||
log_path = (vim.fn.stdpath("cache") .. "/gitlab.nvim.log"),
|
log_path = (vim.fn.stdpath("cache") .. "/gitlab.nvim.log"),
|
||||||
config_path = nil,
|
config_path = nil,
|
||||||
reviewer = "diffview",
|
reviewer = "diffview",
|
||||||
@@ -56,6 +92,7 @@ M.settings = {
|
|||||||
delete_comment = "dd",
|
delete_comment = "dd",
|
||||||
open_in_browser = "b",
|
open_in_browser = "b",
|
||||||
copy_node_url = "u",
|
copy_node_url = "u",
|
||||||
|
publish_draft = "P",
|
||||||
reply = "r",
|
reply = "r",
|
||||||
toggle_node = "t",
|
toggle_node = "t",
|
||||||
add_emoji = "Ea",
|
add_emoji = "Ea",
|
||||||
@@ -72,24 +109,8 @@ M.settings = {
|
|||||||
unresolved = "-",
|
unresolved = "-",
|
||||||
tree_type = "simple",
|
tree_type = "simple",
|
||||||
toggle_tree_type = "i",
|
toggle_tree_type = "i",
|
||||||
---@param t WinbarTable
|
toggle_draft_mode = "D",
|
||||||
winbar = function(t)
|
draft_mode = false,
|
||||||
local discussions_content = t.resolvable_discussions ~= 0
|
|
||||||
and string.format("Discussions (%d/%d)", t.resolved_discussions, t.resolvable_discussions)
|
|
||||||
or "Discussions"
|
|
||||||
local notes_content = t.resolvable_notes ~= 0
|
|
||||||
and string.format("Notes (%d/%d)", t.resolved_notes, t.resolvable_notes)
|
|
||||||
or "Notes"
|
|
||||||
if t.name == "Discussions" then
|
|
||||||
notes_content = "%#Comment#" .. notes_content
|
|
||||||
discussions_content = "%#Text#" .. discussions_content
|
|
||||||
else
|
|
||||||
discussions_content = "%#Comment#" .. discussions_content
|
|
||||||
notes_content = "%#Text#" .. notes_content
|
|
||||||
end
|
|
||||||
local help = "%#Comment#%=Help: " .. t.help_keymap:gsub(" ", "<space>") .. " "
|
|
||||||
return " " .. discussions_content .. " %#Comment#| " .. notes_content .. help
|
|
||||||
end,
|
|
||||||
},
|
},
|
||||||
create_mr = {
|
create_mr = {
|
||||||
target = nil,
|
target = nil,
|
||||||
@@ -101,6 +122,9 @@ M.settings = {
|
|||||||
border = "rounded",
|
border = "rounded",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
choose_merge_request = {
|
||||||
|
open_reviewer = true,
|
||||||
|
},
|
||||||
info = {
|
info = {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
horizontal = false,
|
horizontal = false,
|
||||||
@@ -156,6 +180,7 @@ M.settings = {
|
|||||||
file_name = "Normal",
|
file_name = "Normal",
|
||||||
resolved = "DiagnosticSignOk",
|
resolved = "DiagnosticSignOk",
|
||||||
unresolved = "DiagnosticSignWarn",
|
unresolved = "DiagnosticSignWarn",
|
||||||
|
draft = "DiffviewNonText",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -218,32 +243,13 @@ M.setPluginConfiguration = function()
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local base_path
|
local token, url, err = M.settings.auth_provider()
|
||||||
if M.settings.config_path ~= nil then
|
if err ~= nil then
|
||||||
base_path = M.settings.config_path
|
return
|
||||||
else
|
|
||||||
base_path = vim.fn.trim(vim.fn.system({ "git", "rev-parse", "--show-toplevel" }))
|
|
||||||
if vim.v.shell_error ~= 0 then
|
|
||||||
u.notify(string.format("Could not get base directory: %s", base_path), vim.log.levels.ERROR)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local config_file_path = base_path .. M.settings.file_separator .. ".gitlab.nvim"
|
M.settings.auth_token = token
|
||||||
local config_file_content = u.read_file(config_file_path, { remove_newlines = true })
|
M.settings.gitlab_url = u.trim_slash(url or "https://gitlab.com")
|
||||||
|
|
||||||
local file_properties = {}
|
|
||||||
if config_file_content ~= nil then
|
|
||||||
local file = assert(io.open(config_file_path, "r"))
|
|
||||||
for line in file:lines() do
|
|
||||||
for key, value in string.gmatch(line, "(.-)=(.-)$") do
|
|
||||||
file_properties[key] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
M.settings.auth_token = file_properties.auth_token or os.getenv("GITLAB_TOKEN")
|
|
||||||
M.settings.gitlab_url = u.trim_slash(file_properties.gitlab_url or os.getenv("GITLAB_URL") or "https://gitlab.com")
|
|
||||||
|
|
||||||
if M.settings.auth_token == nil then
|
if M.settings.auth_token == nil then
|
||||||
vim.notify(
|
vim.notify(
|
||||||
@@ -322,17 +328,65 @@ end
|
|||||||
-- for each of the actions to occur. This is necessary because some Gitlab behaviors (like
|
-- for each of the actions to occur. This is necessary because some Gitlab behaviors (like
|
||||||
-- adding a reviewer) requires some initial state.
|
-- adding a reviewer) requires some initial state.
|
||||||
M.dependencies = {
|
M.dependencies = {
|
||||||
user = { endpoint = "/users/me", key = "user", state = "USER", refresh = false },
|
user = {
|
||||||
info = { endpoint = "/mr/info", key = "info", state = "INFO", refresh = false },
|
endpoint = "/users/me",
|
||||||
latest_pipeline = { endpoint = "/pipeline", key = "latest_pipeline", state = "PIPELINE", refresh = true },
|
key = "user",
|
||||||
labels = { endpoint = "/mr/label", key = "labels", state = "LABELS", refresh = false },
|
state = "USER",
|
||||||
revisions = { endpoint = "/mr/revisions", key = "Revisions", state = "MR_REVISIONS", refresh = false },
|
refresh = false,
|
||||||
|
},
|
||||||
|
info = {
|
||||||
|
endpoint = "/mr/info",
|
||||||
|
key = "info",
|
||||||
|
state = "INFO",
|
||||||
|
refresh = false,
|
||||||
|
},
|
||||||
|
latest_pipeline = {
|
||||||
|
endpoint = "/pipeline",
|
||||||
|
key = "latest_pipeline",
|
||||||
|
state = "PIPELINE",
|
||||||
|
refresh = true,
|
||||||
|
},
|
||||||
|
labels = {
|
||||||
|
endpoint = "/mr/label",
|
||||||
|
key = "labels",
|
||||||
|
state = "LABELS",
|
||||||
|
refresh = false,
|
||||||
|
},
|
||||||
|
revisions = {
|
||||||
|
endpoint = "/mr/revisions",
|
||||||
|
key = "Revisions",
|
||||||
|
state = "MR_REVISIONS",
|
||||||
|
refresh = false,
|
||||||
|
},
|
||||||
|
draft_notes = {
|
||||||
|
endpoint = "/mr/draft_notes/",
|
||||||
|
key = "draft_notes",
|
||||||
|
state = "DRAFT_NOTES",
|
||||||
|
refresh = false,
|
||||||
|
},
|
||||||
project_members = {
|
project_members = {
|
||||||
endpoint = "/project/members",
|
endpoint = "/project/members",
|
||||||
key = "ProjectMembers",
|
key = "ProjectMembers",
|
||||||
state = "PROJECT_MEMBERS",
|
state = "PROJECT_MEMBERS",
|
||||||
refresh = false,
|
refresh = false,
|
||||||
},
|
},
|
||||||
|
merge_requests = {
|
||||||
|
endpoint = "/merge_requests",
|
||||||
|
key = "merge_requests",
|
||||||
|
state = "MERGE_REQUESTS",
|
||||||
|
refresh = false,
|
||||||
|
},
|
||||||
|
discussion_data = {
|
||||||
|
endpoint = "/mr/discussions/list",
|
||||||
|
state = "DISCUSSION_DATA",
|
||||||
|
refresh = false,
|
||||||
|
method = "POST",
|
||||||
|
body = function()
|
||||||
|
return {
|
||||||
|
blacklist = M.settings.discussion_tree.blacklist,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
-- This function clears out all of the previously fetched data. It's used
|
-- This function clears out all of the previously fetched data. It's used
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
local git = require("gitlab.git")
|
||||||
local List = require("gitlab.utils.list")
|
local List = require("gitlab.utils.list")
|
||||||
local has_devicons, devicons = pcall(require, "nvim-web-devicons")
|
local has_devicons, devicons = pcall(require, "nvim-web-devicons")
|
||||||
local M = {}
|
local M = {}
|
||||||
@@ -202,6 +203,17 @@ M.split_by_new_lines = function(s)
|
|||||||
return s:gmatch("(.-)\n") -- Match 0 or more (as few as possible) characters followed by a new line.
|
return s:gmatch("(.-)\n") -- Match 0 or more (as few as possible) characters followed by a new line.
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Takes a string of lines and returns a table of lines
|
||||||
|
---@param s string The string to parse
|
||||||
|
---@return table
|
||||||
|
M.lines_into_table = function(s)
|
||||||
|
local lines = {}
|
||||||
|
for line in M.split_by_new_lines(s) do
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
-- Reverses the order of elements in a list
|
-- Reverses the order of elements in a list
|
||||||
---@param list table The list to reverse
|
---@param list table The list to reverse
|
||||||
---@return table
|
---@return table
|
||||||
@@ -493,7 +505,7 @@ M.create_popup_state = function(title, settings, width, height, zindex)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---Create view_opts for Box popups used inside popup Layouts
|
---Create view_opts for Box popups used inside popup Layouts
|
||||||
---@param title string The string to appear on top of the popup
|
---@param title string|nil The string to appear on top of the popup
|
||||||
---@param enter boolean Whether the pop should be focused after creation
|
---@param enter boolean Whether the pop should be focused after creation
|
||||||
---@return table
|
---@return table
|
||||||
M.create_box_popup_state = function(title, enter)
|
M.create_box_popup_state = function(title, enter)
|
||||||
@@ -656,52 +668,10 @@ M.make_comma_separated_readable = function(str)
|
|||||||
return string.gsub(str, ",", ", ")
|
return string.gsub(str, ",", ", ")
|
||||||
end
|
end
|
||||||
|
|
||||||
---Return the name of the current branch
|
|
||||||
---@return string|nil
|
|
||||||
M.get_current_branch = function()
|
|
||||||
local handle = io.popen("git branch --show-current 2>&1")
|
|
||||||
if handle then
|
|
||||||
return handle:read()
|
|
||||||
else
|
|
||||||
M.notify("Error running 'git branch' command.", vim.log.levels.ERROR)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---Return the list of names of all remote-tracking branches
|
|
||||||
M.get_all_merge_targets = function()
|
|
||||||
local handle = io.popen("git branch -r 2>&1")
|
|
||||||
if not handle then
|
|
||||||
M.notify("Error running 'git branch' command.", vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local current_branch = M.get_current_branch()
|
|
||||||
if not current_branch then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local lines = {}
|
|
||||||
for line in handle:lines() do
|
|
||||||
table.insert(lines, line)
|
|
||||||
end
|
|
||||||
handle:close()
|
|
||||||
|
|
||||||
-- Trim "origin/" and don't include the HEAD pointer
|
|
||||||
local branches = List.new(lines)
|
|
||||||
:map(function(line)
|
|
||||||
return line:match("origin/(%S+)")
|
|
||||||
end)
|
|
||||||
:filter(function(branch)
|
|
||||||
return not branch:match("^HEAD$") and branch ~= current_branch
|
|
||||||
end)
|
|
||||||
|
|
||||||
return branches
|
|
||||||
end
|
|
||||||
|
|
||||||
---Select a git branch and perform callback with the branch as an argument
|
---Select a git branch and perform callback with the branch as an argument
|
||||||
---@param cb function The callback to perform with the selected branch
|
---@param cb function The callback to perform with the selected branch
|
||||||
M.select_target_branch = function(cb)
|
M.select_target_branch = function(cb)
|
||||||
local all_branch_names = M.get_all_merge_targets()
|
local all_branch_names = git.get_all_merge_targets()
|
||||||
if not all_branch_names then
|
if not all_branch_names then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -738,6 +708,20 @@ M.open_in_browser = function(url)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Combines two tables
|
||||||
|
---@param t1 table
|
||||||
|
---@param t2 table
|
||||||
|
---@return table
|
||||||
|
M.join = function(t1, t2)
|
||||||
|
local res = {}
|
||||||
|
for _, val in ipairs(t1) do
|
||||||
|
table.insert(res, val)
|
||||||
|
end
|
||||||
|
for _, val in ipairs(t2) do
|
||||||
|
table.insert(res, val)
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
---Trims the trailing slash from a URL
|
---Trims the trailing slash from a URL
|
||||||
---@param s string
|
---@param s string
|
||||||
---@return string
|
---@return string
|
||||||
@@ -745,4 +729,11 @@ M.trim_slash = function(s)
|
|||||||
return (s:gsub("/+$", ""))
|
return (s:gsub("/+$", ""))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M.ensure_table = function(data)
|
||||||
|
if data == vim.NIL or data == nil then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ end
|
|||||||
|
|
||||||
---Filters a given list
|
---Filters a given list
|
||||||
---@generic T
|
---@generic T
|
||||||
---@param func fun(v: T):boolean
|
---@param func fun(v: T, i: integer):boolean
|
||||||
---@return List<T> @Returns a new list of elements for which func returns true
|
---@return List<T> @Returns a new list of elements for which func returns true
|
||||||
function List:filter(func)
|
function List:filter(func)
|
||||||
local result = List.new()
|
local result = List.new()
|
||||||
for _, v in ipairs(self) do
|
for i, v in ipairs(self) do
|
||||||
if func(v) == true then
|
if func(v, i) == true then
|
||||||
table.insert(result, v)
|
table.insert(result, v)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -63,6 +63,19 @@ function List:slice(first, last, step)
|
|||||||
return sliced
|
return sliced
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Returns true if any of the elements can satisfy the callback
|
||||||
|
---@generic T
|
||||||
|
---@param func fun(v: T, i: integer):boolean
|
||||||
|
---@return List<T> @Returns a boolean
|
||||||
|
function List:includes(func)
|
||||||
|
for i, v in ipairs(self) do
|
||||||
|
if func(v, i) == true then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
function List:values()
|
function List:values()
|
||||||
local result = {}
|
local result = {}
|
||||||
for _, v in ipairs(self) do
|
for _, v in ipairs(self) do
|
||||||
|
|||||||
Reference in New Issue
Block a user