Simplify Go Endpoints + Add Tests (#120)
This MR represents a major refactor of the Go codebase, as well as introducing tests for the handlers. The MR also introduces an endpoint to shutdown or restart the Go server, which may be useful for clients who want to refresh the state of the plugin after checking out branches. Finally, this MR adds a contributing document for users who want to make feature changes.
This commit is contained in:
committed by
GitHub
parent
10b0b596ae
commit
93fe3e8bd6
52
.github/CONTRIBUTING.md
vendored
Normal file
52
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Contributing to gitlab.nvim
|
||||||
|
|
||||||
|
Thank you for taking time to contribute to this plugin! Please follow these steps when creating a feature.
|
||||||
|
|
||||||
|
1. If the functionality you want is not a bug fix, please create a "feature request" issue first
|
||||||
|
|
||||||
|
It's possible that the feature you want is already implemented, or does not belong in `gitlab.nvim` at all. By creating an issue first you can have a conversation with the maintainers about the functionality first. While this is not strictly necessary, it greatly increases the likelihood that your merge request will be accepted.
|
||||||
|
|
||||||
|
2. Fork the repository, and create a new feature branch for your desired functionality. Make your changes.
|
||||||
|
|
||||||
|
If you are using Lazy as a plugin manager, the easiest way to work on changes is by setting a specific path for the plugin that points to your repository locally. This is what I do:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
"harrisoncramer/gitlab.nvim",
|
||||||
|
dependencies = {
|
||||||
|
"MunifTanjim/nui.nvim",
|
||||||
|
"nvim-lua/plenary.nvim",
|
||||||
|
},
|
||||||
|
build = function()
|
||||||
|
require("gitlab.server").build()
|
||||||
|
end,
|
||||||
|
dir = "~/.path/to/your-closed-version", -- Pass in the path to your cloned repository
|
||||||
|
config = function()
|
||||||
|
require("gitlab").setup({})
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are making changes to the Go codebase, don't forget to run `make compile` in the root of the project to rebuild the binary!
|
||||||
|
|
||||||
|
3. Apply formatters and linters to your changes
|
||||||
|
|
||||||
|
For changes to the Go codbase: We use <a href="https://pkg.go.dev/cmd/gofmt">gofmt</a> to check formatting and <a href="https://github.com/golangci/golangci-lint">golangci-lint</a> to check linting. Run these commands in the root of the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ stylua .
|
||||||
|
$ luacheck --globals vim busted --no-max-line-length -- .
|
||||||
|
```
|
||||||
|
|
||||||
|
For changes to the Lua codebase: We use <a href="https://github.com/JohnnyMorganz/StyLua">stylua</a> for formatting and <a href="https://github.com/mpeterv/luacheck">luacheck</a> for linting. Run these commands in the root of the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ go fmt ./...
|
||||||
|
$ golangci-lint run
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Make the merge request to the `main` branch of `.gitlab.nvim`
|
||||||
|
|
||||||
|
Please provide a description of the feature, and links to any relevant issues.
|
||||||
|
|
||||||
|
That's it! I'll try to respond to any incoming merge request in a few days. Once we've reviewed it and it's been merged into main, the pipeline will detect whether we're merging in a patch, minor, or major change, and create a new tag (e.g. 1.0.12) and release.
|
||||||
23
README.md
23
README.md
@@ -29,6 +29,7 @@ https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/dc5c07de-4ae6-4335
|
|||||||
- [MR Approvals](#mr-approvals)
|
- [MR Approvals](#mr-approvals)
|
||||||
- [Pipelines](#pipelines)
|
- [Pipelines](#pipelines)
|
||||||
- [Reviewers and Assignees](#reviewers-and-assignees)
|
- [Reviewers and Assignees](#reviewers-and-assignees)
|
||||||
|
- [Restarting or Shutting down](#restarting-or-shutting-down)
|
||||||
- [Keybindings](#keybindings)
|
- [Keybindings](#keybindings)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
@@ -324,6 +325,27 @@ require("dressing").setup({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Restarting or Shutting Down
|
||||||
|
|
||||||
|
The `gitlab.nvim` server will shut down automatically when you exit Neovim. However, if you would like to manage this yourself (for instance, restart the server when you check out a new branch) you may do so via the `restart` command, or `shutdown` commands, which
|
||||||
|
both accept callbacks.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
require("gitlab.server").restart()
|
||||||
|
```
|
||||||
|
|
||||||
|
For instance you could set up the following keybinding to close and reopen the reviewer when checking out a new branch:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local gitlab = require("gitlab")
|
||||||
|
vim.keymap.set("n", "glB", function ()
|
||||||
|
require("gitlab.server").restart(function ()
|
||||||
|
vim.cmd.tabclose()
|
||||||
|
gitlab.review() -- Reopen the reviewer after the server restarts
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
## Keybindings
|
## Keybindings
|
||||||
|
|
||||||
The plugin does not set up any keybindings outside of the special buffers it creates,
|
The plugin does not set up any keybindings outside of the special buffers it creates,
|
||||||
@@ -333,6 +355,7 @@ as `gl` does not have a special meaning in normal mode):
|
|||||||
|
|
||||||
```lua
|
```lua
|
||||||
local gitlab = require("gitlab")
|
local gitlab = require("gitlab")
|
||||||
|
local gitlab_server = require("gitlab.server")
|
||||||
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)
|
||||||
|
|||||||
@@ -2,36 +2,38 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ApproveHandler(w http.ResponseWriter, r *http.Request) {
|
/* approveHandler approves a merge request. */
|
||||||
|
func (a *api) approveHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
|
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
w.Header().Set("Allow", http.MethodPost)
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, res, err := c.git.MergeRequestApprovals.ApproveMergeRequest(c.projectId, c.mergeId, nil, nil)
|
_, res, err := a.client.ApproveMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, nil, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not approve MR", http.StatusBadRequest)
|
handleError(w, err, "Could not approve merge request", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Check for non-200 status codes */
|
if res.StatusCode >= 300 {
|
||||||
w.WriteHeader(res.StatusCode)
|
handleError(w, GenericError{endpoint: "/approve"}, "Could not approve merge request", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{
|
||||||
Message: "Success! Approved MR.",
|
Message: "Approved MR",
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
cmd/approve_test.go
Normal file
52
cmd/approve_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func approveMergeRequest(pid interface{}, mr int, opt *gitlab.ApproveMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequestApprovals, *gitlab.Response, error) {
|
||||||
|
return &gitlab.MergeRequestApprovals{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func approveMergeRequestNon200(pid interface{}, mr int, opt *gitlab.ApproveMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequestApprovals, *gitlab.Response, error) {
|
||||||
|
return &gitlab.MergeRequestApprovals{}, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func approveMergeRequestErr(pid interface{}, mr int, opt *gitlab.ApproveMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequestApprovals, *gitlab.Response, error) {
|
||||||
|
return &gitlab.MergeRequestApprovals{}, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApproveHandler(t *testing.T) {
|
||||||
|
t.Run("Approves merge request", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/approve", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequest})
|
||||||
|
data := serveRequest(t, server, request, SuccessResponse{})
|
||||||
|
assert(t, data.Message, "Approved MR")
|
||||||
|
assert(t, data.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disallows non-POST method", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPut, "/approve", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequest})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkBadMethod(t, *data, http.MethodPost)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/approve", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequestErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not approve merge request")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/approve", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequestNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not approve merge request", "/approve")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -22,13 +22,18 @@ type AssigneesRequestResponse struct {
|
|||||||
Assignees []int `json:"assignees"`
|
Assignees []int `json:"assignees"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssigneesHandler(w http.ResponseWriter, r *http.Request) {
|
/* assigneesHandler adds or removes assignees from a merge request. */
|
||||||
c := r.Context().Value("client").(Client)
|
func (a *api) assigneesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if r.Method != http.MethodPut {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodPut)
|
||||||
|
handleError(w, InvalidRequestError{}, "Expected PUT", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,26 +42,25 @@ func AssigneesHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
err = json.Unmarshal(body, &assigneeUpdateRequest)
|
err = json.Unmarshal(body, &assigneeUpdateRequest)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mr, res, err := c.git.MergeRequests.UpdateMergeRequest(c.projectId, c.mergeId, &gitlab.UpdateMergeRequestOptions{
|
mr, res, err := a.client.UpdateMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.UpdateMergeRequestOptions{
|
||||||
AssigneeIDs: &assigneeUpdateRequest.Ids,
|
AssigneeIDs: &assigneeUpdateRequest.Ids,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not modify merge request assignees", http.StatusBadRequest)
|
handleError(w, err, "Could not modify merge request assignees", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode >= 300 {
|
||||||
c.handleError(w, err, "Could not modify merge request assignees", http.StatusBadRequest)
|
handleError(w, GenericError{endpoint: "/mr/assignee"}, "Could not modify merge request assignees", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
response := AssigneeUpdateResponse{
|
response := AssigneeUpdateResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Message: "Assignees updated",
|
Message: "Assignees updated",
|
||||||
@@ -67,6 +71,6 @@ func AssigneesHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
cmd/assignee_test.go
Normal file
58
cmd/assignee_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func updateAssignees(pid interface{}, mergeRequest int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return &gitlab.MergeRequest{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAssigneesNon200(pid interface{}, mergeRequest int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAssigneesErr(pid interface{}, mergeRequest int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssigneeHandler(t *testing.T) {
|
||||||
|
t.Run("Updates assignees", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPut, "/mr/assignee", AssigneeUpdateRequest{Ids: []int{1, 2}})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequestFn: updateAssignees})
|
||||||
|
data := serveRequest(t, server, request, AssigneeUpdateResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Assignees updated")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disallows non-PUT method", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/assignee", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequestFn: updateAssignees})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Status, http.StatusMethodNotAllowed)
|
||||||
|
assert(t, data.Details, "Invalid request type")
|
||||||
|
assert(t, data.Message, "Expected PUT")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPut, "/mr/assignee", AssigneeUpdateRequest{Ids: []int{1, 2}})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequestFn: updateAssigneesErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Status, http.StatusInternalServerError)
|
||||||
|
assert(t, data.Message, "Could not modify merge request assignees")
|
||||||
|
assert(t, data.Details, "Some error from Gitlab")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPut, "/mr/assignee", AssigneeUpdateRequest{Ids: []int{1, 2}})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequestFn: updateAssigneesNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
assert(t, data.Status, http.StatusSeeOther)
|
||||||
|
assert(t, data.Message, "Could not modify merge request assignees")
|
||||||
|
assert(t, data.Details, "An error occurred on the /mr/assignee endpoint")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -8,6 +9,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type FileReader interface {
|
||||||
|
ReadFile(path string) (io.Reader, error)
|
||||||
|
}
|
||||||
|
|
||||||
type AttachmentRequest struct {
|
type AttachmentRequest struct {
|
||||||
FilePath string `json:"file_path"`
|
FilePath string `json:"file_path"`
|
||||||
FileName string `json:"file_name"`
|
FileName string `json:"file_name"`
|
||||||
@@ -20,18 +25,40 @@ type AttachmentResponse struct {
|
|||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func AttachmentHandler(w http.ResponseWriter, r *http.Request) {
|
type attachmentReader struct{}
|
||||||
|
|
||||||
|
func (ar attachmentReader) ReadFile(path string) (io.Reader, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* attachmentHandler uploads an attachment (file, image, etc) to Gitlab and returns metadata about the upload. */
|
||||||
|
func (a *api) attachmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
||||||
|
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
var attachmentRequest AttachmentRequest
|
var attachmentRequest AttachmentRequest
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,21 +66,23 @@ func AttachmentHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.Unmarshal(body, &attachmentRequest)
|
err = json.Unmarshal(body, &attachmentRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not unmarshal JSON", http.StatusBadRequest)
|
handleError(w, err, "Could not unmarshal JSON", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(attachmentRequest.FilePath)
|
file, err := a.fileReader.ReadFile(attachmentRequest.FileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, fmt.Sprintf("Could not read %s", attachmentRequest.FilePath), http.StatusBadRequest)
|
handleError(w, err, fmt.Sprintf("Could not read %s file", attachmentRequest.FileName), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFile, res, err := a.client.UploadFile(a.projectInfo.ProjectId, file, attachmentRequest.FileName)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FileName), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer file.Close()
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/mr/attachment"}, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FileName), res.StatusCode)
|
||||||
projectFile, res, err := c.git.Projects.UploadFile(c.projectId, file, attachmentRequest.FileName)
|
|
||||||
if err != nil {
|
|
||||||
c.handleError(w, err, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FilePath), res.StatusCode)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +98,6 @@ func AttachmentHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
cmd/attachment_test.go
Normal file
66
cmd/attachment_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockAttachmentReader struct{}
|
||||||
|
|
||||||
|
func (mf MockAttachmentReader) ReadFile(path string) (io.Reader, error) {
|
||||||
|
return bytes.NewReader([]byte{}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadFile(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error) {
|
||||||
|
return &gitlab.ProjectFile{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadFileNon200(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error) {
|
||||||
|
return &gitlab.ProjectFile{}, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadFileErr(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func withMockFileReader(a *api) error {
|
||||||
|
reader := MockAttachmentReader{}
|
||||||
|
a.fileReader = reader
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttachmentHandler(t *testing.T) {
|
||||||
|
t.Run("Returns 200-status response after upload", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||||
|
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFile}, withMockFileReader)
|
||||||
|
data := serveRequest(t, router, request, AttachmentResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
assert(t, data.SuccessResponse.Message, "File uploaded successfully")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disallows non-POST method", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPut, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||||
|
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFile}, withMockFileReader)
|
||||||
|
data := serveRequest(t, router, request, ErrorResponse{})
|
||||||
|
checkBadMethod(t, *data, http.MethodPost)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||||
|
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFileErr}, withMockFileReader)
|
||||||
|
data := serveRequest(t, router, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not upload some_file_name to Gitlab")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
|
||||||
|
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFileNon200}, withMockFileReader)
|
||||||
|
data := serveRequest(t, router, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not upload some_file_name to Gitlab", "/mr/attachment")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -14,34 +14,42 @@ import (
|
|||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
projectId string
|
|
||||||
mergeId int
|
|
||||||
gitlabInstance string
|
|
||||||
authToken string
|
|
||||||
git *gitlab.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
type DebugSettings struct {
|
type DebugSettings struct {
|
||||||
GoRequest bool `json:"go_request"`
|
GoRequest bool `json:"go_request"`
|
||||||
GoResponse bool `json:"go_response"`
|
GoResponse bool `json:"go_response"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This will parse and validate the project settings and then initialize the Gitlab client */
|
type ProjectInfo struct {
|
||||||
func (c *Client) initGitlabClient() error {
|
ProjectId string
|
||||||
|
MergeId int
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Client struct embeds all the methods from Gitlab for the different services */
|
||||||
|
type Client struct {
|
||||||
|
*gitlab.MergeRequestsService
|
||||||
|
*gitlab.MergeRequestApprovalsService
|
||||||
|
*gitlab.DiscussionsService
|
||||||
|
*gitlab.ProjectsService
|
||||||
|
*gitlab.ProjectMembersService
|
||||||
|
*gitlab.JobsService
|
||||||
|
*gitlab.PipelinesService
|
||||||
|
}
|
||||||
|
|
||||||
|
/* initGitlabClient parses and validates the project settings and initializes the Gitlab client. */
|
||||||
|
func initGitlabClient() (error, *Client) {
|
||||||
|
|
||||||
if len(os.Args) < 6 {
|
if len(os.Args) < 6 {
|
||||||
return errors.New("Must provide gitlab url, port, auth token, debug settings, and log path")
|
return errors.New("Must provide gitlab url, port, auth token, debug settings, and log path"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gitlabInstance := os.Args[1]
|
gitlabInstance := os.Args[1]
|
||||||
if gitlabInstance == "" {
|
if gitlabInstance == "" {
|
||||||
return errors.New("GitLab instance URL cannot be empty")
|
return errors.New("GitLab instance URL cannot be empty"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
authToken := os.Args[3]
|
authToken := os.Args[3]
|
||||||
if authToken == "" {
|
if authToken == "" {
|
||||||
return errors.New("Auth token cannot be empty")
|
return errors.New("Auth token cannot be empty"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Parse debug settings and initialize logger handlers */
|
/* Parse debug settings and initialize logger handlers */
|
||||||
@@ -49,7 +57,7 @@ func (c *Client) initGitlabClient() error {
|
|||||||
var debugObject DebugSettings
|
var debugObject DebugSettings
|
||||||
err := json.Unmarshal([]byte(debugSettings), &debugObject)
|
err := json.Unmarshal([]byte(debugSettings), &debugObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Could not parse debug settings: %w, %s", err, debugSettings)
|
return fmt.Errorf("Could not parse debug settings: %w, %s", err, debugSettings), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiCustUrl = fmt.Sprintf(gitlabInstance + "/api/v4")
|
var apiCustUrl = fmt.Sprintf(gitlabInstance + "/api/v4")
|
||||||
@@ -66,65 +74,70 @@ func (c *Client) initGitlabClient() error {
|
|||||||
gitlabOptions = append(gitlabOptions, gitlab.WithResponseLogHook(responseLogger))
|
gitlabOptions = append(gitlabOptions, gitlab.WithResponseLogHook(responseLogger))
|
||||||
}
|
}
|
||||||
|
|
||||||
git, err := gitlab.NewClient(authToken, gitlabOptions...)
|
client, err := gitlab.NewClient(authToken, gitlabOptions...)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to create client: %v", err)
|
return fmt.Errorf("Failed to create client: %v", err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.gitlabInstance = gitlabInstance
|
return nil, &Client{
|
||||||
c.authToken = authToken
|
MergeRequestsService: client.MergeRequests,
|
||||||
c.git = git
|
MergeRequestApprovalsService: client.MergeRequestApprovals,
|
||||||
|
DiscussionsService: client.Discussions,
|
||||||
return nil
|
ProjectsService: client.Projects,
|
||||||
|
ProjectMembersService: client.ProjectMembers,
|
||||||
|
JobsService: client.Jobs,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This will fetch the project ID and merge request ID using the client */
|
/* initProjectSettings fetch the project ID and merge request ID using the client. */
|
||||||
func (c *Client) initProjectSettings(g GitProjectInfo) error {
|
func initProjectSettings(c *Client, gitInfo GitProjectInfo) (error, *ProjectInfo) {
|
||||||
|
|
||||||
opt := gitlab.GetProjectOptions{}
|
opt := gitlab.GetProjectOptions{}
|
||||||
project, _, err := c.git.Projects.GetProject(g.projectPath(), &opt)
|
project, _, err := c.GetProject(gitInfo.projectPath(), &opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(fmt.Sprintf("Error getting project at %s", g.RemoteUrl), err)
|
return fmt.Errorf(fmt.Sprintf("Error getting project at %s", gitInfo.RemoteUrl), err), nil
|
||||||
}
|
}
|
||||||
if project == nil {
|
if project == nil {
|
||||||
return fmt.Errorf(fmt.Sprintf("Could not find project at %s", g.RemoteUrl), err)
|
return fmt.Errorf(fmt.Sprintf("Could not find project at %s", gitInfo.RemoteUrl), err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if project == nil {
|
if project == nil {
|
||||||
return fmt.Errorf("No projects you are a member of contained remote URL %s", g.RemoteUrl)
|
return fmt.Errorf("No projects you are a member of contained remote URL %s", gitInfo.RemoteUrl), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.projectId = fmt.Sprint(project.ID)
|
projectId := fmt.Sprint(project.ID)
|
||||||
|
|
||||||
options := gitlab.ListProjectMergeRequestsOptions{
|
options := gitlab.ListProjectMergeRequestsOptions{
|
||||||
Scope: gitlab.String("all"),
|
Scope: gitlab.String("all"),
|
||||||
State: gitlab.String("opened"),
|
State: gitlab.String("opened"),
|
||||||
SourceBranch: &g.BranchName,
|
SourceBranch: &gitInfo.BranchName,
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeRequests, _, err := c.git.MergeRequests.ListProjectMergeRequests(c.projectId, &options)
|
mergeRequests, _, err := c.ListProjectMergeRequests(projectId, &options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to list merge requests: %w", err)
|
return fmt.Errorf("Failed to list merge requests: %w", err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(mergeRequests) == 0 {
|
if len(mergeRequests) == 0 {
|
||||||
return errors.New("No merge requests found")
|
return errors.New("No merge requests found"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeId := strconv.Itoa(mergeRequests[0].IID)
|
mergeId := strconv.Itoa(mergeRequests[0].IID)
|
||||||
mergeIdInt, err := strconv.Atoi(mergeId)
|
mergeIdInt, err := strconv.Atoi(mergeId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mergeId = mergeIdInt
|
return nil, &ProjectInfo{
|
||||||
|
MergeId: mergeIdInt,
|
||||||
return nil
|
ProjectId: projectId,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) handleError(w http.ResponseWriter, err error, message string, status int) {
|
/* handleError is a utililty handler that returns errors to the client along with their statuses and messages */
|
||||||
|
func handleError(w http.ResponseWriter, err error, message string, status int) {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
response := ErrorResponse{
|
response := ErrorResponse{
|
||||||
Message: message,
|
Message: message,
|
||||||
@@ -134,7 +147,7 @@ func (c *Client) handleError(w http.ResponseWriter, err error, message string, s
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode error response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
115
cmd/comment.go
115
cmd/comment.go
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -23,14 +22,13 @@ type PostCommentRequest struct {
|
|||||||
LineRange *LineRange `json:"line_range,omitempty"`
|
LineRange *LineRange `json:"line_range,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LineRange represents the range of a note.
|
/* LineRange represents the range of a note. */
|
||||||
type LineRange struct {
|
type LineRange struct {
|
||||||
StartRange *LinePosition `json:"start"`
|
StartRange *LinePosition `json:"start"`
|
||||||
EndRange *LinePosition `json:"end"`
|
EndRange *LinePosition `json:"end"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LinePosition represents a position in a line range.
|
/* LinePosition represents a position in a line range. Unlike the Gitlab struct, this does not contain LineCode with a sha1 of the filename */
|
||||||
// unlike gitlab struct this does not contain LineCode with sha1 of filename
|
|
||||||
type LinePosition struct {
|
type LinePosition struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
OldLine int `json:"old_line"`
|
OldLine int `json:"old_line"`
|
||||||
@@ -55,26 +53,28 @@ type CommentResponse struct {
|
|||||||
Discussion *gitlab.Discussion `json:"discussion"`
|
Discussion *gitlab.Discussion `json:"discussion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func CommentHandler(w http.ResponseWriter, r *http.Request) {
|
/* commentHandler creates, edits, and deletes discussions (comments, multi-line comments) */
|
||||||
|
func (a *api) commentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodDelete:
|
|
||||||
DeleteComment(w, r)
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
PostComment(w, r)
|
a.postComment(w, r)
|
||||||
case http.MethodPatch:
|
case http.MethodPatch:
|
||||||
EditComment(w, r)
|
a.editComment(w, r)
|
||||||
|
case http.MethodDelete:
|
||||||
|
a.deleteComment(w, r)
|
||||||
default:
|
default:
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteComment(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
c := r.Context().Value("client").(Client)
|
w.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, %s, %s", http.MethodDelete, http.MethodPost, http.MethodPatch))
|
||||||
|
handleError(w, InvalidRequestError{}, "Expected DELETE, POST or PATCH", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* deleteComment deletes a note, multiline comment, or comment, which are all considered discussion notes. */
|
||||||
|
func (a *api) deleteComment(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,37 +83,41 @@ func DeleteComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
var deleteCommentRequest DeleteCommentRequest
|
var deleteCommentRequest DeleteCommentRequest
|
||||||
err = json.Unmarshal(body, &deleteCommentRequest)
|
err = json.Unmarshal(body, &deleteCommentRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := c.git.Discussions.DeleteMergeRequestDiscussionNote(c.projectId, c.mergeId, deleteCommentRequest.DiscussionId, deleteCommentRequest.NoteId)
|
res, err := a.client.DeleteMergeRequestDiscussionNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, deleteCommentRequest.DiscussionId, deleteCommentRequest.NoteId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not delete comment", res.StatusCode)
|
handleError(w, err, "Could not delete comment", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/comment"}, "Could not delete comment", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Check status code */
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{
|
||||||
Message: "Comment deleted succesfully",
|
Message: "Comment deleted successfully",
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func PostComment(w http.ResponseWriter, r *http.Request) {
|
/* postComment creates a note, multiline comment, or comment. */
|
||||||
|
func (a *api) postComment(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +126,7 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
var postCommentRequest PostCommentRequest
|
var postCommentRequest PostCommentRequest
|
||||||
err = json.Unmarshal(body, &postCommentRequest)
|
err = json.Unmarshal(body, &postCommentRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +136,9 @@ func 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 != "" {
|
if postCommentRequest.FileName != "" {
|
||||||
|
friendlyName = "Comment"
|
||||||
opt.Position = &gitlab.PositionOptions{
|
opt.Position = &gitlab.PositionOptions{
|
||||||
PositionType: &postCommentRequest.Type,
|
PositionType: &postCommentRequest.Type,
|
||||||
StartSHA: &postCommentRequest.StartCommitSHA,
|
StartSHA: &postCommentRequest.StartCommitSHA,
|
||||||
@@ -145,15 +151,16 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if postCommentRequest.LineRange != nil {
|
if postCommentRequest.LineRange != nil {
|
||||||
var format = "%x_%d_%d"
|
friendlyName = "Multiline Comment"
|
||||||
var start_filename_sha1 = fmt.Sprintf(
|
shaFormat := "%x_%d_%d"
|
||||||
format,
|
startFilenameSha := fmt.Sprintf(
|
||||||
|
shaFormat,
|
||||||
sha1.Sum([]byte(postCommentRequest.FileName)),
|
sha1.Sum([]byte(postCommentRequest.FileName)),
|
||||||
postCommentRequest.LineRange.StartRange.OldLine,
|
postCommentRequest.LineRange.StartRange.OldLine,
|
||||||
postCommentRequest.LineRange.StartRange.NewLine,
|
postCommentRequest.LineRange.StartRange.NewLine,
|
||||||
)
|
)
|
||||||
var end_filename_sha1 = fmt.Sprintf(
|
endFilenameSha := fmt.Sprintf(
|
||||||
format,
|
shaFormat,
|
||||||
sha1.Sum([]byte(postCommentRequest.FileName)),
|
sha1.Sum([]byte(postCommentRequest.FileName)),
|
||||||
postCommentRequest.LineRange.EndRange.OldLine,
|
postCommentRequest.LineRange.EndRange.OldLine,
|
||||||
postCommentRequest.LineRange.EndRange.NewLine,
|
postCommentRequest.LineRange.EndRange.NewLine,
|
||||||
@@ -161,26 +168,32 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
opt.Position.LineRange = &gitlab.LineRangeOptions{
|
opt.Position.LineRange = &gitlab.LineRangeOptions{
|
||||||
Start: &gitlab.LinePositionOptions{
|
Start: &gitlab.LinePositionOptions{
|
||||||
Type: &postCommentRequest.LineRange.StartRange.Type,
|
Type: &postCommentRequest.LineRange.StartRange.Type,
|
||||||
LineCode: &start_filename_sha1,
|
LineCode: &startFilenameSha,
|
||||||
},
|
},
|
||||||
End: &gitlab.LinePositionOptions{
|
End: &gitlab.LinePositionOptions{
|
||||||
Type: &postCommentRequest.LineRange.EndRange.Type,
|
Type: &postCommentRequest.LineRange.EndRange.Type,
|
||||||
LineCode: &end_filename_sha1,
|
LineCode: &endFilenameSha,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
discussion, _, err := c.git.Discussions.CreateMergeRequestDiscussion(c.projectId, c.mergeId, &opt)
|
discussion, res, err := a.client.CreateMergeRequestDiscussion(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not create comment", http.StatusBadRequest)
|
handleError(w, err, "Could not create discussion", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/comment"}, "Could not create discussion", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
response := CommentResponse{
|
response := CommentResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Message: "Comment updated succesfully",
|
Message: fmt.Sprintf("%s created successfully", friendlyName),
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
},
|
},
|
||||||
Comment: discussion.Notes[0],
|
Comment: discussion.Notes[0],
|
||||||
@@ -189,17 +202,17 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EditComment(w http.ResponseWriter, r *http.Request) {
|
/* editComment changes the text of a comment or changes it's resolved status. */
|
||||||
|
func (a *api) editComment(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,31 +221,29 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
var editCommentRequest EditCommentRequest
|
var editCommentRequest EditCommentRequest
|
||||||
err = json.Unmarshal(body, &editCommentRequest)
|
err = json.Unmarshal(body, &editCommentRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
options := gitlab.UpdateMergeRequestDiscussionNoteOptions{}
|
options := gitlab.UpdateMergeRequestDiscussionNoteOptions{}
|
||||||
|
|
||||||
msg := "edit comment"
|
|
||||||
options.Body = gitlab.String(editCommentRequest.Comment)
|
options.Body = gitlab.String(editCommentRequest.Comment)
|
||||||
|
|
||||||
note, res, err := c.git.Discussions.UpdateMergeRequestDiscussionNote(c.projectId, c.mergeId, editCommentRequest.DiscussionId, editCommentRequest.NoteId, &options)
|
note, res, err := a.client.UpdateMergeRequestDiscussionNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, editCommentRequest.DiscussionId, editCommentRequest.NoteId, &options)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not "+msg, res.StatusCode)
|
handleError(w, err, "Could not update comment", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(res.StatusCode)
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/comment"}, "Could not update comment", res.StatusCode)
|
||||||
if res.StatusCode != http.StatusOK {
|
return
|
||||||
c.handleError(w, errors.New("Non-200 status code recieved"), "Could not "+msg, res.StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
response := CommentResponse{
|
response := CommentResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Message: "Comment updated succesfully",
|
Message: "Comment updated successfully",
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
},
|
},
|
||||||
Comment: note,
|
Comment: note,
|
||||||
@@ -240,6 +251,6 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
cmd/comment_test.go
Normal file
139
cmd/comment_test.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createMergeRequestDiscussion(pid interface{}, mergeRequest int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
return &gitlab.Discussion{Notes: []*gitlab.Note{{}}}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMergeRequestDiscussionNon200(pid interface{}, mergeRequest int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMergeRequestDiscussionErr(pid interface{}, mergeRequest int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostComment(t *testing.T) {
|
||||||
|
t.Run("Creates a new note (unlinked comment)", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
||||||
|
data := serveRequest(t, server, request, CommentResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Note created successfully")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Creates a new comment", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{FileName: "some_file.txt"})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
||||||
|
data := serveRequest(t, server, request, CommentResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Comment created successfully")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Creates a new multiline comment", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{
|
||||||
|
FileName: "some_file.txt",
|
||||||
|
LineRange: &LineRange{
|
||||||
|
StartRange: &LinePosition{}, /* These would have real data */
|
||||||
|
EndRange: &LinePosition{},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
|
||||||
|
data := serveRequest(t, server, request, CommentResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Multiline Comment created successfully")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussionErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not create discussion")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussionNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not create discussion", "/comment")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteMergeRequestDiscussionNoteErr(pid interface{}, mergeRequest int, discussion string, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteMergeRequestDiscussionNoteNon200(pid interface{}, mergeRequest int, discussion string, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteComment(t *testing.T) {
|
||||||
|
t.Run("Deletes a comment", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodDelete, "/comment", DeleteCommentRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{deleteMergeRequestDiscussionNote: deleteMergeRequestDiscussionNote})
|
||||||
|
data := serveRequest(t, server, request, CommentResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Comment deleted successfully")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodDelete, "/comment", DeleteCommentRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{deleteMergeRequestDiscussionNote: deleteMergeRequestDiscussionNoteErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not delete comment")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodDelete, "/comment", DeleteCommentRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{deleteMergeRequestDiscussionNote: deleteMergeRequestDiscussionNoteNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not delete comment", "/comment")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error) {
|
||||||
|
return &gitlab.Note{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMergeRequestDiscussionNoteErr(pid interface{}, mergeRequest int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMergeRequestDiscussionNoteNon200(pid interface{}, mergeRequest int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditComment(t *testing.T) {
|
||||||
|
t.Run("Edits a comment", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPatch, "/comment", EditCommentRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequestDiscussionNote: updateMergeRequestDiscussionNote})
|
||||||
|
data := serveRequest(t, server, request, CommentResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Comment updated successfully")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPatch, "/comment", EditCommentRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequestDiscussionNote: updateMergeRequestDiscussionNoteErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not update comment")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPatch, "/comment", EditCommentRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{updateMergeRequestDiscussionNote: updateMergeRequestDiscussionNoteNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not update comment", "/comment")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ Extracts information about the current repository and returns
|
|||||||
it to the client for initialization. The current directory must be a valid
|
it to the client for initialization. The current directory must be a valid
|
||||||
Gitlab project and the branch must be a feature branch
|
Gitlab project and the branch must be a feature branch
|
||||||
*/
|
*/
|
||||||
func ExtractGitInfo(refreshGitInfo func() error, getProjectRemoteUrl func() (string, error), getCurrentBranchName func() (string, error)) (GitProjectInfo, error) {
|
func extractGitInfo(refreshGitInfo func() error, getProjectRemoteUrl func() (string, error), getCurrentBranchName func() (string, error)) (GitProjectInfo, error) {
|
||||||
|
|
||||||
err := refreshGitInfo()
|
err := refreshGitInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
251
cmd/git_test.go
251
cmd/git_test.go
@@ -1,251 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestExtractGitInfo_Success(t *testing.T) {
|
|
||||||
getCurrentBranchName := func() (string, error) {
|
|
||||||
return "feature/abc", nil
|
|
||||||
}
|
|
||||||
refreshGitInfo := func() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
testCases := []struct {
|
|
||||||
getProjectRemoteUrl func() (string, error)
|
|
||||||
expected GitProjectInfo
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "Project configured in SSH under a single folder",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "git@custom-gitlab.com:namespace-1/project-name.git", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "git@custom-gitlab.com:namespace-1/project-name.git",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in SSH under a single folder without .git extension",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "git@custom-gitlab.com:namespace-1/project-name", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "git@custom-gitlab.com:namespace-1/project-name",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in SSH under one nested folder",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "git@custom-gitlab.com:namespace-1/namespace-2/project-name.git", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "git@custom-gitlab.com:namespace-1/namespace-2/project-name.git",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1/namespace-2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in SSH under two nested folders",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "git@custom-gitlab.com:namespace-1/namespace-2/namespace-3/project-name.git", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "git@custom-gitlab.com:namespace-1/namespace-2/namespace-3/project-name.git",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1/namespace-2/namespace-3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in SSH:// under a single folder",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "ssh://custom-gitlab.com/namespace-1/project-name.git", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "ssh://custom-gitlab.com/namespace-1/project-name.git",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in SSH:// under a single folder without .git extension",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "ssh://custom-gitlab.com/namespace-1/project-name", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "ssh://custom-gitlab.com/namespace-1/project-name",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in SSH:// under two nested folders",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "ssh://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "ssh://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1/namespace-2/namespace-3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in HTTP and under a single folder without .git extension",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "http://custom-gitlab.com/namespace-1/project-name", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "http://custom-gitlab.com/namespace-1/project-name",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in HTTPS and under a single folder",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "https://custom-gitlab.com/namespace-1/project-name.git", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "https://custom-gitlab.com/namespace-1/project-name.git",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in HTTPS and under a nested folder",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "https://custom-gitlab.com/namespace-1/namespace-2/project-name.git", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "https://custom-gitlab.com/namespace-1/namespace-2/project-name.git",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1/namespace-2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Project configured in HTTPS and under two nested folders",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "https://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git", nil
|
|
||||||
},
|
|
||||||
expected: GitProjectInfo{
|
|
||||||
RemoteUrl: "https://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git",
|
|
||||||
BranchName: "feature/abc",
|
|
||||||
ProjectName: "project-name",
|
|
||||||
Namespace: "namespace-1/namespace-2/namespace-3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tC := range testCases {
|
|
||||||
t.Run(tC.desc, func(t *testing.T) {
|
|
||||||
actual, err := ExtractGitInfo(refreshGitInfo, tC.getProjectRemoteUrl, getCurrentBranchName)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("No error was expected, got %s", err)
|
|
||||||
}
|
|
||||||
if actual != tC.expected {
|
|
||||||
t.Errorf("\nExpected: %s\nActual: %s", tC.expected, actual)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractGitInfo_FailToGetProjectRemoteUrl(t *testing.T) {
|
|
||||||
getCurrentBranchName := func() (string, error) {
|
|
||||||
return "feature/abc", nil
|
|
||||||
}
|
|
||||||
refreshGitInfo := func() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
testCases := []struct {
|
|
||||||
getProjectRemoteUrl func() (string, error)
|
|
||||||
expectedErrorMessage string
|
|
||||||
desc string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "Error returned by function to get the project remote url",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "", errors.New("error when getting project remote url")
|
|
||||||
},
|
|
||||||
expectedErrorMessage: "Could not get project Url: error when getting project remote url",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Invalid project remote url",
|
|
||||||
getProjectRemoteUrl: func() (string, error) {
|
|
||||||
return "git@invalid", nil
|
|
||||||
},
|
|
||||||
expectedErrorMessage: "Invalid Git URL format: git@invalid",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tC := range testCases {
|
|
||||||
t.Run(tC.desc, func(t *testing.T) {
|
|
||||||
_, actualErr := ExtractGitInfo(refreshGitInfo, tC.getProjectRemoteUrl, getCurrentBranchName)
|
|
||||||
if actualErr == nil {
|
|
||||||
t.Errorf("Expected an error, got none")
|
|
||||||
}
|
|
||||||
if actualErr.Error() != tC.expectedErrorMessage {
|
|
||||||
t.Errorf("\nExpected: %s\nActual: %s", tC.expectedErrorMessage, actualErr.Error())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractGitInfo_FailToGetCurrentBranchName(t *testing.T) {
|
|
||||||
expectedErrNestedMsg := "error when getting current branch name"
|
|
||||||
|
|
||||||
refreshGitInfo := func() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_, actualErr := ExtractGitInfo(refreshGitInfo,
|
|
||||||
func() (string, error) {
|
|
||||||
return "git@custom-gitlab.com:namespace/project.git", nil
|
|
||||||
},
|
|
||||||
func() (string, error) {
|
|
||||||
return "", errors.New(expectedErrNestedMsg)
|
|
||||||
})
|
|
||||||
|
|
||||||
if actualErr == nil {
|
|
||||||
t.Errorf("Expected an error, got none")
|
|
||||||
}
|
|
||||||
expectedErr := fmt.Errorf("Failed to get current branch: %s", expectedErrNestedMsg)
|
|
||||||
if actualErr.Error() != expectedErr.Error() {
|
|
||||||
t.Errorf("\nExpected: %s\nActual: %s", expectedErr, actualErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRefreshGitRemote_FailToRefreshRemote(t *testing.T) {
|
|
||||||
expectedErrNestedMsg := "error when fetching origin commits"
|
|
||||||
_, actualErr := ExtractGitInfo(
|
|
||||||
func() error {
|
|
||||||
return errors.New(expectedErrNestedMsg)
|
|
||||||
},
|
|
||||||
func() (string, error) {
|
|
||||||
return "git@custom-gitlab.com:namespace/project.git", nil
|
|
||||||
},
|
|
||||||
func() (string, error) {
|
|
||||||
return "feature/abc", nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if actualErr == nil {
|
|
||||||
t.Errorf("Expected an error, got none")
|
|
||||||
}
|
|
||||||
expectedErr := fmt.Errorf("Could not get latest information from remote: %s", expectedErrNestedMsg)
|
|
||||||
if actualErr.Error() != expectedErr.Error() {
|
|
||||||
t.Errorf("\nExpected: %s\nActual: %s", expectedErr, actualErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
62
cmd/info.go
62
cmd/info.go
@@ -2,75 +2,33 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
const mrUrl = "%s/api/v4/projects/%s/merge_requests/%d"
|
|
||||||
|
|
||||||
type InfoResponse struct {
|
type InfoResponse struct {
|
||||||
SuccessResponse
|
SuccessResponse
|
||||||
Info *gitlab.MergeRequest `json:"info"`
|
Info *gitlab.MergeRequest `json:"info"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Info() ([]byte, error) {
|
/* infoHandler fetches infomation about the current git project. The data returned here is used in many other API calls */
|
||||||
|
func (a *api) infoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
url := fmt.Sprintf(mrUrl, c.gitlabInstance, c.projectId, c.mergeId)
|
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to build read request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("PRIVATE-TOKEN", c.authToken)
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to make info request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
||||||
return nil, fmt.Errorf("Recieved non-200 response: %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to parse read response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* This response is parsed into a table in our Lua code */
|
|
||||||
return body, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func InfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
|
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
w.Header().Set("Allow", http.MethodGet)
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := c.Info()
|
mr, res, err := a.client.GetMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.GetMergeRequestsOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not get project info and initialize gitlab.nvim plugin", http.StatusBadRequest)
|
handleError(w, err, "Could not get project info", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var mergeRequest *gitlab.MergeRequest
|
if res.StatusCode >= 300 {
|
||||||
err = json.Unmarshal(msg, &mergeRequest)
|
handleError(w, GenericError{endpoint: "/info"}, "Could not get project info", res.StatusCode)
|
||||||
if err != nil {
|
|
||||||
c.handleError(w, err, "Could not unmarshal data from merge requests", http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +38,11 @@ func InfoHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
Message: "Merge requests retrieved",
|
Message: "Merge requests retrieved",
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
},
|
},
|
||||||
Info: mergeRequest,
|
Info: mr,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
cmd/info_test.go
Normal file
53
cmd/info_test.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInfo(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return &gitlab.MergeRequest{Title: "Some Title"}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInfoNon200(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInfoErr(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInfoHandler(t *testing.T) {
|
||||||
|
t.Run("Returns normal information", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/info", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfo})
|
||||||
|
data := serveRequest(t, server, request, InfoResponse{})
|
||||||
|
assert(t, data.Info.Title, "Some Title")
|
||||||
|
assert(t, data.SuccessResponse.Message, "Merge requests retrieved")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disallows non-GET method", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/info", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfo})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkBadMethod(t, *data, http.MethodGet)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/info", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfoErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not get project info")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/info", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfoNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not get project info", "/info")
|
||||||
|
})
|
||||||
|
}
|
||||||
32
cmd/job.go
32
cmd/job.go
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
@@ -16,19 +15,19 @@ type JobTraceResponse struct {
|
|||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func JobHandler(w http.ResponseWriter, r *http.Request) {
|
/* jobHandler returns a string that shows the output of a specific job run in a Gitlab pipeline */
|
||||||
|
func (a *api) jobHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
|
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
w.Header().Set("Allow", http.MethodGet)
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
@@ -36,18 +35,27 @@ func JobHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var jobTraceRequest JobTraceRequest
|
var jobTraceRequest JobTraceRequest
|
||||||
err = json.Unmarshal(body, &jobTraceRequest)
|
err = json.Unmarshal(body, &jobTraceRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, _, err := c.git.Jobs.GetTraceFile(c.projectId, jobTraceRequest.JobId)
|
reader, res, err := a.client.GetTraceFile(a.projectInfo.ProjectId, jobTraceRequest.JobId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not get trace file for job", http.StatusBadRequest)
|
handleError(w, err, "Could not get trace file for job", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/job"}, "Could not get trace file for job", res.StatusCode)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := io.ReadAll(reader)
|
file, err := io.ReadAll(reader)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read job trace file", http.StatusBadRequest)
|
handleError(w, err, "Could not read job trace file", http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := JobTraceResponse{
|
response := JobTraceResponse{
|
||||||
@@ -60,6 +68,6 @@ func JobHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
cmd/job_test.go
Normal file
54
cmd/job_test.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTraceFile(pid interface{}, jobID int, options ...gitlab.RequestOptionFunc) (*bytes.Reader, *gitlab.Response, error) {
|
||||||
|
return bytes.NewReader([]byte("Some data")), makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTraceFileErr(pid interface{}, jobID int, options ...gitlab.RequestOptionFunc) (*bytes.Reader, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTraceFileNon200(pid interface{}, jobID int, options ...gitlab.RequestOptionFunc) (*bytes.Reader, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobHandler(t *testing.T) {
|
||||||
|
t.Run("Should read a job trace file", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/job", JobTraceRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{getTraceFile: getTraceFile})
|
||||||
|
data := serveRequest(t, server, request, JobTraceResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Log file read")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
assert(t, data.File, "Some data")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disallows non-GET methods", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/job", JobTraceRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{getTraceFile: getTraceFile})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkBadMethod(t, *data, http.MethodGet)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should handle errors from Gitlab", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/job", JobTraceRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{getTraceFile: getTraceFileErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not get trace file for job")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should handle non-200s", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/job", JobTraceRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{getTraceFile: getTraceFileNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not get trace file for job", "/job")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -38,16 +36,47 @@ func (n SortableDiscussions) Swap(i, j int) {
|
|||||||
n[i], n[j] = n[j], n[i]
|
n[i], n[j] = n[j], n[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ListDiscussions(blacklist []string) ([]*gitlab.Discussion, []*gitlab.Discussion, int, error) {
|
/*
|
||||||
|
listDiscussionsHandler lists all discusions for a given merge request, both those linked and unlinked to particular points in the code.
|
||||||
|
The responses are sorted by date created, and blacklisted users are not included
|
||||||
|
*/
|
||||||
|
func (a *api) listDiscussionsHandler(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var requestBody DiscussionsRequest
|
||||||
|
err = json.Unmarshal(body, &requestBody)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not unmarshal request body", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
mergeRequestDiscussionOptions := gitlab.ListMergeRequestDiscussionsOptions{
|
mergeRequestDiscussionOptions := gitlab.ListMergeRequestDiscussionsOptions{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PerPage: 250,
|
PerPage: 250,
|
||||||
}
|
}
|
||||||
discussions, res, err := c.git.Discussions.ListMergeRequestDiscussions(c.projectId, c.mergeId, &mergeRequestDiscussionOptions, nil)
|
|
||||||
|
discussions, res, err := a.client.ListMergeRequestDiscussions(a.projectInfo.ProjectId, a.projectInfo.MergeId, &mergeRequestDiscussionOptions, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, res.Response.StatusCode, fmt.Errorf("Listing discussions failed: %w", err)
|
handleError(w, err, "Could not list discussions", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/discussions/list"}, "Could not list discussions", res.StatusCode)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filter out any discussions started by a blacklisted user
|
/* Filter out any discussions started by a blacklisted user
|
||||||
@@ -55,7 +84,7 @@ func (c *Client) ListDiscussions(blacklist []string) ([]*gitlab.Discussion, []*g
|
|||||||
var unlinkedDiscussions []*gitlab.Discussion
|
var unlinkedDiscussions []*gitlab.Discussion
|
||||||
var linkedDiscussions []*gitlab.Discussion
|
var linkedDiscussions []*gitlab.Discussion
|
||||||
for _, discussion := range discussions {
|
for _, discussion := range discussions {
|
||||||
if Contains(blacklist, discussion.Notes[0].Author.Username) > -1 {
|
if discussion.Notes == nil || len(discussion.Notes) == 0 || Contains(requestBody.Blacklist, discussion.Notes[0].Author.Username) > -1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, note := range discussion.Notes {
|
for _, note := range discussion.Notes {
|
||||||
@@ -75,43 +104,15 @@ func (c *Client) ListDiscussions(blacklist []string) ([]*gitlab.Discussion, []*g
|
|||||||
sort.Sort(sortedLinkedDiscussions)
|
sort.Sort(sortedLinkedDiscussions)
|
||||||
sort.Sort(sortedUnlinkedDiscussions)
|
sort.Sort(sortedUnlinkedDiscussions)
|
||||||
|
|
||||||
return sortedLinkedDiscussions, sortedUnlinkedDiscussions, http.StatusOK, nil
|
if err != nil {
|
||||||
}
|
handleError(w, err, "Could not list discussions", http.StatusBadRequest)
|
||||||
|
|
||||||
func ListDiscussionsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.Header().Set("Allow", http.MethodPost)
|
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestBody DiscussionsRequest
|
|
||||||
err = json.Unmarshal(body, &requestBody)
|
|
||||||
if err != nil {
|
|
||||||
c.handleError(w, err, "Could not unmarshal request body", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
linkedDiscussions, unlinkedDiscussions, status, err := c.ListDiscussions(requestBody.Blacklist)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.handleError(w, err, "Could not list discussions", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TODO: Check for non-200 statuses */
|
|
||||||
w.WriteHeader(status)
|
|
||||||
response := DiscussionsResponse{
|
response := DiscussionsResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Message: "Discussions successfully fetched.",
|
Message: "Discussions retrieved",
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
},
|
},
|
||||||
Discussions: linkedDiscussions,
|
Discussions: linkedDiscussions,
|
||||||
@@ -120,6 +121,6 @@ func ListDiscussionsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
91
cmd/list_discussions_test.go
Normal file
91
cmd/list_discussions_test.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listMergeRequestDiscussions(pid interface{}, mergeRequest int, opt *gitlab.ListMergeRequestDiscussionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
now := time.Now()
|
||||||
|
newer := now.Add(time.Second * 100)
|
||||||
|
discussions := []*gitlab.Discussion{
|
||||||
|
{
|
||||||
|
Notes: []*gitlab.Note{
|
||||||
|
{
|
||||||
|
CreatedAt: &now,
|
||||||
|
Type: "DiffNote",
|
||||||
|
Author: Author{
|
||||||
|
Username: "hcramer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Notes: []*gitlab.Note{
|
||||||
|
{
|
||||||
|
CreatedAt: &newer,
|
||||||
|
Type: "DiffNote",
|
||||||
|
Author: Author{
|
||||||
|
Username: "hcramer2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return discussions, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listMergeRequestDiscussionsErr(pid interface{}, mergeRequest int, opt *gitlab.ListMergeRequestDiscussionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func listMergeRequestDiscussionsNon200(pid interface{}, mergeRequest int, opt *gitlab.ListMergeRequestDiscussionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListDiscussionsHandler(t *testing.T) {
|
||||||
|
t.Run("Returns sorted discussions", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussions})
|
||||||
|
data := serveRequest(t, server, request, DiscussionsResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2") /* Sorting applied */
|
||||||
|
assert(t, data.Discussions[1].Notes[0].Author.Username, "hcramer")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Uses blacklist to filter unwanted authors", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer"}})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussions})
|
||||||
|
data := serveRequest(t, server, request, DiscussionsResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
assert(t, len(data.Discussions), 1)
|
||||||
|
assert(t, data.Discussions[0].Notes[0].Author.Username, "hcramer2")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disallows non-POST method", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPatch, "/discussions/list", DiscussionsRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussions})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkBadMethod(t, *data, http.MethodPost)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussionsErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not list discussions")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussionsNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not list discussions", "/discussions/list")
|
||||||
|
})
|
||||||
|
}
|
||||||
84
cmd/main.go
84
cmd/main.go
@@ -1,96 +1,24 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
g, 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 with `git` commands: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var c Client
|
err, client := initGitlabClient()
|
||||||
if err := c.initGitlabClient(); err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to initialize Gitlab client: %v", err)
|
log.Fatalf("Failed to initialize Gitlab client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.initProjectSettings(g); err != nil {
|
err, projectInfo := initProjectSettings(client, gitInfo)
|
||||||
|
if err != nil {
|
||||||
log.Fatalf("Failed to initialize project settings: %v", err)
|
log.Fatalf("Failed to initialize project settings: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m := http.NewServeMux()
|
startServer(client, projectInfo)
|
||||||
m.Handle("/ping", http.HandlerFunc(PingHandler))
|
|
||||||
m.Handle("/mr/summary", withGitlabContext(http.HandlerFunc(SummaryHandler), c))
|
|
||||||
m.Handle("/mr/attachment", withGitlabContext(http.HandlerFunc(AttachmentHandler), c))
|
|
||||||
m.Handle("/mr/reviewer", withGitlabContext(http.HandlerFunc(ReviewersHandler), c))
|
|
||||||
m.Handle("/mr/revisions", withGitlabContext(http.HandlerFunc(RevisionsHandler), c))
|
|
||||||
m.Handle("/mr/assignee", withGitlabContext(http.HandlerFunc(AssigneesHandler), c))
|
|
||||||
m.Handle("/approve", withGitlabContext(http.HandlerFunc(ApproveHandler), c))
|
|
||||||
m.Handle("/revoke", withGitlabContext(http.HandlerFunc(RevokeHandler), c))
|
|
||||||
m.Handle("/info", withGitlabContext(http.HandlerFunc(InfoHandler), c))
|
|
||||||
m.Handle("/discussions", withGitlabContext(http.HandlerFunc(ListDiscussionsHandler), c))
|
|
||||||
m.Handle("/discussion/resolve", withGitlabContext(http.HandlerFunc(DiscussionResolveHandler), c))
|
|
||||||
m.Handle("/comment", withGitlabContext(http.HandlerFunc(CommentHandler), c))
|
|
||||||
m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c))
|
|
||||||
m.Handle("/members", withGitlabContext(http.HandlerFunc(ProjectMembersHandler), c))
|
|
||||||
m.Handle("/pipeline", withGitlabContext(http.HandlerFunc(PipelineHandler), c))
|
|
||||||
m.Handle("/job", withGitlabContext(http.HandlerFunc(JobHandler), c))
|
|
||||||
|
|
||||||
port := os.Args[2]
|
|
||||||
if port == "" {
|
|
||||||
// port was not specified
|
|
||||||
port = "0"
|
|
||||||
}
|
|
||||||
addr := fmt.Sprintf("localhost:%s", port)
|
|
||||||
listener, err := net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
fmt.Fprintf(os.Stderr, "Error starting server: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
listenerPort := listener.Addr().(*net.TCPAddr).Port
|
|
||||||
|
|
||||||
errCh := make(chan error)
|
|
||||||
go func() {
|
|
||||||
err := http.Serve(listener, m)
|
|
||||||
errCh <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
resp, err := http.Get("http://localhost:" + fmt.Sprintf("%d", listenerPort) + "/ping")
|
|
||||||
if resp.StatusCode == 200 && err == nil {
|
|
||||||
/* This print is detected by the Lua code and used to fetch project information */
|
|
||||||
fmt.Println("Server started on port: ", listenerPort)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Wait for healthcheck to pass - at most 1 sec.
|
|
||||||
time.Sleep(100 * time.Microsecond)
|
|
||||||
}
|
|
||||||
errCh <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := <-errCh; err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error starting server: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func PingHandler(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprintln(w, "pong")
|
|
||||||
}
|
|
||||||
|
|
||||||
func withGitlabContext(next http.HandlerFunc, c Client) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := context.WithValue(context.Background(), "client", c) //nolint:all
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ type ProjectMembersResponse struct {
|
|||||||
ProjectMembers []*gitlab.ProjectMember
|
ProjectMembers []*gitlab.ProjectMember
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProjectMembersHandler(w http.ResponseWriter, r *http.Request) {
|
/* projectMembersHandler returns all members of the current Gitlab project */
|
||||||
c := r.Context().Value("client").(Client)
|
func (a *api) projectMembersHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
||||||
|
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
projectMemberOptions := gitlab.ListProjectMembersOptions{
|
projectMemberOptions := gitlab.ListProjectMembersOptions{
|
||||||
ListOptions: gitlab.ListOptions{
|
ListOptions: gitlab.ListOptions{
|
||||||
@@ -22,9 +27,16 @@ func ProjectMembersHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
projectMembers, res, err := c.git.ProjectMembers.ListAllProjectMembers(c.projectId, &projectMemberOptions)
|
projectMembers, res, err := a.client.ListAllProjectMembers(a.projectInfo.ProjectId, &projectMemberOptions)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not fetch project users", res.StatusCode)
|
handleError(w, err, "Could not retrieve project members", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/project/members"}, "Could not retrieve project members", res.StatusCode)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -32,13 +44,13 @@ func ProjectMembersHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
response := ProjectMembersResponse{
|
response := ProjectMembersResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Message: "Project users fetched successfully",
|
Message: "Project members retrieved",
|
||||||
},
|
},
|
||||||
ProjectMembers: projectMembers,
|
ProjectMembers: projectMembers,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
cmd/members_test.go
Normal file
52
cmd/members_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listAllProjectMembers(pid interface{}, opt *gitlab.ListProjectMembersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectMember, *gitlab.Response, error) {
|
||||||
|
return []*gitlab.ProjectMember{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listAllProjectMembersErr(pid interface{}, opt *gitlab.ListProjectMembersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectMember, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func listAllProjectMembersNon200(pid interface{}, opt *gitlab.ListProjectMembersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectMember, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMembersHandler(t *testing.T) {
|
||||||
|
t.Run("Returns project members", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/project/members", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listAllProjectMembers: listAllProjectMembers})
|
||||||
|
data := serveRequest(t, server, request, ProjectMembersResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Project members retrieved")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disallows non-GET method", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/project/members", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listAllProjectMembers: listAllProjectMembers})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkBadMethod(t, *data, http.MethodGet)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/project/members", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listAllProjectMembers: listAllProjectMembersErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not retrieve project members")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/project/members", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listAllProjectMembers: listAllProjectMembersNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not retrieve project members", "/project/members")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,16 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PipelineRequest struct {
|
|
||||||
PipelineId int `json:"pipeline_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RetriggerPipelineResponse struct {
|
type RetriggerPipelineResponse struct {
|
||||||
SuccessResponse
|
SuccessResponse
|
||||||
Pipeline *gitlab.Pipeline
|
Pipeline *gitlab.Pipeline
|
||||||
@@ -22,80 +20,82 @@ type GetJobsResponse struct {
|
|||||||
Jobs []*gitlab.Job
|
Jobs []*gitlab.Job
|
||||||
}
|
}
|
||||||
|
|
||||||
func PipelineHandler(w http.ResponseWriter, r *http.Request) {
|
/*
|
||||||
|
pipelineHandler fetches information about the current pipeline, and retriggers a pipeline run. For more detailed information
|
||||||
|
about a given job in a pipeline, see the jobHandler function
|
||||||
|
*/
|
||||||
|
func (a *api) pipelineHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
GetJobs(w, r)
|
a.GetJobs(w, r)
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
RetriggerPipeline(w, r)
|
a.RetriggerPipeline(w, r)
|
||||||
default:
|
default:
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetJobs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
c := r.Context().Value("client").(Client)
|
w.Header().Set("Access-Control-Allow-Methods", fmt.Sprintf("%s, %s", http.MethodGet, http.MethodPost))
|
||||||
|
handleError(w, InvalidRequestError{}, "Expected GET or POST", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *api) GetJobs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
id := strings.TrimPrefix(r.URL.Path, "/pipeline/")
|
||||||
|
idInt, err := strconv.Atoi(id)
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not convert pipeline ID to integer", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer r.Body.Close()
|
jobs, res, err := a.client.ListPipelineJobs(a.projectInfo.ProjectId, idInt, &gitlab.ListJobsOptions{})
|
||||||
|
|
||||||
var pipelineRequest PipelineRequest
|
|
||||||
err = json.Unmarshal(body, &pipelineRequest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read JSON", http.StatusBadRequest)
|
handleError(w, err, "Could not get pipeline jobs", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs, res, err := c.git.Jobs.ListPipelineJobs(c.projectId, pipelineRequest.PipelineId, &gitlab.ListJobsOptions{})
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/pipeline"}, "Could not get pipeline jobs", res.StatusCode)
|
||||||
if err != nil {
|
return
|
||||||
c.handleError(w, err, "Could not get pipeline jobs", res.StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
response := GetJobsResponse{
|
response := GetJobsResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Message: "Jobs fetched successfully",
|
Message: "Pipeline jobs retrieved",
|
||||||
},
|
},
|
||||||
Jobs: jobs,
|
Jobs: jobs,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
func (a *api) RetriggerPipeline(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func RetriggerPipeline(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
id := strings.TrimPrefix(r.URL.Path, "/pipeline/")
|
||||||
|
|
||||||
|
idInt, err := strconv.Atoi(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not convert pipeline ID to integer", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer r.Body.Close()
|
pipeline, res, err := a.client.RetryPipelineBuild(a.projectInfo.ProjectId, idInt)
|
||||||
|
|
||||||
var pipelineRequest PipelineRequest
|
|
||||||
err = json.Unmarshal(body, &pipelineRequest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read JSON", http.StatusBadRequest)
|
handleError(w, err, "Could not retrigger pipeline", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline, res, err := c.git.Pipelines.RetryPipelineBuild(c.projectId, pipelineRequest.PipelineId)
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/pipeline"}, "Could not retrigger pipeline", res.StatusCode)
|
||||||
if err != nil {
|
return
|
||||||
c.handleError(w, err, "Could not retrigger pipeline", res.StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -109,6 +109,6 @@ func RetriggerPipeline(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
86
cmd/pipeline_test.go
Normal file
86
cmd/pipeline_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listPipelineJobs(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error) {
|
||||||
|
return []*gitlab.Job{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listPipelineJobsErr(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func listPipelineJobsNon200(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func retryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) {
|
||||||
|
return &gitlab.Pipeline{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func retryPipelineBuildErr(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func retryPipelineBuildNon200(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipelineHandler(t *testing.T) {
|
||||||
|
t.Run("Gets all pipeline jobs", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/pipeline/1", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listPipelineJobs: listPipelineJobs})
|
||||||
|
data := serveRequest(t, server, request, GetJobsResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Pipeline jobs retrieved")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disallows non-GET, non-POST methods", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPatch, "/pipeline/1", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listPipelineJobs: listPipelineJobs})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkBadMethod(t, *data, http.MethodGet, http.MethodPost)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/pipeline/1", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listPipelineJobs: listPipelineJobsErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not get pipeline jobs")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/pipeline/1", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{listPipelineJobs: listPipelineJobsNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not get pipeline jobs", "/pipeline")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Retriggers pipeline", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/pipeline/1", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{retryPipelineBuild: retryPipelineBuild})
|
||||||
|
data := serveRequest(t, server, request, GetJobsResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Pipeline retriggered")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/pipeline/1", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{retryPipelineBuild: retryPipelineBuildErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not retrigger pipeline")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab client on retrigger", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/pipeline/1", nil)
|
||||||
|
server, _ := createRouterAndApi(fakeClient{retryPipelineBuild: retryPipelineBuildNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not retrigger pipeline", "/pipeline")
|
||||||
|
})
|
||||||
|
}
|
||||||
53
cmd/reply.go
53
cmd/reply.go
@@ -2,8 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@@ -21,36 +19,18 @@ type ReplyResponse struct {
|
|||||||
Note *gitlab.Note `json:"note"`
|
Note *gitlab.Note `json:"note"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Reply(r ReplyRequest) (*gitlab.Note, int, error) {
|
/* replyHandler sends a reply to a note or comment */
|
||||||
|
func (a *api) replyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
now := time.Now()
|
|
||||||
options := gitlab.AddMergeRequestDiscussionNoteOptions{
|
|
||||||
Body: gitlab.String(r.Reply),
|
|
||||||
CreatedAt: &now,
|
|
||||||
}
|
|
||||||
|
|
||||||
note, res, err := c.git.Discussions.AddMergeRequestDiscussionNote(c.projectId, c.mergeId, r.DiscussionId, &options)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, res.Response.StatusCode, fmt.Errorf("Could not leave reply: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return note, http.StatusOK, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReplyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
w.Header().Set("Allow", http.MethodPost)
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,21 +39,32 @@ func ReplyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
err = json.Unmarshal(body, &replyRequest)
|
err = json.Unmarshal(body, &replyRequest)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
note, status, err := c.Reply(replyRequest)
|
now := time.Now()
|
||||||
|
options := gitlab.AddMergeRequestDiscussionNoteOptions{
|
||||||
|
Body: gitlab.String(replyRequest.Reply),
|
||||||
|
CreatedAt: &now,
|
||||||
|
}
|
||||||
|
|
||||||
|
note, res, err := a.client.AddMergeRequestDiscussionNote(a.projectInfo.ProjectId, a.projectInfo.MergeId, replyRequest.DiscussionId, &options)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not send reply", status)
|
handleError(w, err, "Could not leave reply", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(status)
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/reply"}, "Could not leave reply", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
response := ReplyResponse{
|
response := ReplyResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Message: fmt.Sprintf("Replied: %s", note.Body),
|
Message: "Replied to comment",
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
},
|
},
|
||||||
Note: note,
|
Note: note,
|
||||||
@@ -81,6 +72,6 @@ func ReplyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
cmd/reply_test.go
Normal file
52
cmd/reply_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, opt *gitlab.AddMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error) {
|
||||||
|
return &gitlab.Note{}, makeResponse(http.StatusOK), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMergeRequestDiscussionNoteErr(pid interface{}, mergeRequest int, discussion string, opt *gitlab.AddMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error) {
|
||||||
|
return nil, nil, errors.New("Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMergeRequestDiscussionNoteNon200(pid interface{}, mergeRequest int, discussion string, opt *gitlab.AddMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error) {
|
||||||
|
return nil, makeResponse(http.StatusSeeOther), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplyHandler(t *testing.T) {
|
||||||
|
t.Run("Sends a reply", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/reply", ReplyRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNote})
|
||||||
|
data := serveRequest(t, server, request, ReplyResponse{})
|
||||||
|
assert(t, data.SuccessResponse.Message, "Replied to comment")
|
||||||
|
assert(t, data.SuccessResponse.Status, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disallows non-POST methods", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodGet, "/reply", ReplyRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNote})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkBadMethod(t, *data, http.MethodPost)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/reply", ReplyRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNoteErr})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkErrorFromGitlab(t, *data, "Could not leave reply")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
|
||||||
|
request := makeRequest(t, http.MethodPost, "/reply", ReplyRequest{})
|
||||||
|
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNoteNon200})
|
||||||
|
data := serveRequest(t, server, request, ErrorResponse{})
|
||||||
|
checkNon200(t, *data, "Could not leave reply", "/reply")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -13,57 +14,61 @@ type DiscussionResolveRequest struct {
|
|||||||
Resolved bool `json:"resolved"`
|
Resolved bool `json:"resolved"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DiscussionResolveHandler(w http.ResponseWriter, r *http.Request) {
|
/* discussionsResolveHandler sets a discussion to be "resolved" or not resolved, depending on the payload */
|
||||||
switch r.Method {
|
func (a *api) discussionsResolveHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
case http.MethodPut:
|
|
||||||
DiscussionResolve(w, r)
|
|
||||||
default:
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func DiscussionResolve(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
c := r.Context().Value("client").(Client)
|
if r.Method != http.MethodPut {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodPut)
|
||||||
|
handleError(w, InvalidRequestError{}, "Expected PUT", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
return
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
var resolveDiscussionRequest DiscussionResolveRequest
|
|
||||||
err = json.Unmarshal(body, &resolveDiscussionRequest)
|
|
||||||
if err != nil {
|
|
||||||
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, res, err := c.git.Discussions.ResolveMergeRequestDiscussion(
|
defer r.Body.Close()
|
||||||
c.projectId,
|
|
||||||
c.mergeId,
|
var resolveDiscussionRequest DiscussionResolveRequest
|
||||||
|
err = json.Unmarshal(body, &resolveDiscussionRequest)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, res, err := a.client.ResolveMergeRequestDiscussion(
|
||||||
|
a.projectInfo.ProjectId,
|
||||||
|
a.projectInfo.MergeId,
|
||||||
resolveDiscussionRequest.DiscussionID,
|
resolveDiscussionRequest.DiscussionID,
|
||||||
&gitlab.ResolveMergeRequestDiscussionOptions{Resolved: &resolveDiscussionRequest.Resolved},
|
&gitlab.ResolveMergeRequestDiscussionOptions{Resolved: &resolveDiscussionRequest.Resolved},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
friendlyName := "unresolve"
|
||||||
|
if resolveDiscussionRequest.Resolved {
|
||||||
|
friendlyName = "resolve"
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not update resolve status of discussion", res.StatusCode)
|
handleError(w, err, fmt.Sprintf("Could not %s discussion", friendlyName), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/discussions/resolve"}, fmt.Sprintf("Could not %s discussion", friendlyName), res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
var message string
|
|
||||||
if resolveDiscussionRequest.Resolved {
|
|
||||||
message = "Discussion resolved"
|
|
||||||
} else {
|
|
||||||
message = "Discussion unresolved"
|
|
||||||
}
|
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{
|
||||||
Message: message,
|
Message: fmt.Sprintf("Discussion %sd", friendlyName),
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,18 @@ type ReviewersRequestResponse struct {
|
|||||||
Reviewers []int `json:"reviewers"`
|
Reviewers []int `json:"reviewers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReviewersHandler(w http.ResponseWriter, r *http.Request) {
|
/* reviewersHandler adds or removes reviewers from an MR */
|
||||||
c := r.Context().Value("client").(Client)
|
func (a *api) reviewersHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if r.Method != http.MethodPut {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodPut)
|
||||||
|
handleError(w, InvalidRequestError{}, "Expected PUT", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,26 +42,25 @@ func ReviewersHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
err = json.Unmarshal(body, &reviewerUpdateRequest)
|
err = json.Unmarshal(body, &reviewerUpdateRequest)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mr, res, err := c.git.MergeRequests.UpdateMergeRequest(c.projectId, c.mergeId, &gitlab.UpdateMergeRequestOptions{
|
mr, res, err := a.client.UpdateMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.UpdateMergeRequestOptions{
|
||||||
ReviewerIDs: &reviewerUpdateRequest.Ids,
|
ReviewerIDs: &reviewerUpdateRequest.Ids,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not modify merge request reviewers", http.StatusBadRequest)
|
handleError(w, err, "Could not modify merge request reviewers", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode >= 300 {
|
||||||
c.handleError(w, err, "Could not modify merge request reviewers", http.StatusBadRequest)
|
handleError(w, GenericError{endpoint: "/mr/reviewer"}, "Could not modify merge request reviewers", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
response := ReviewerUpdateResponse{
|
response := ReviewerUpdateResponse{
|
||||||
SuccessResponse: SuccessResponse{
|
SuccessResponse: SuccessResponse{
|
||||||
Message: "Reviewers updated",
|
Message: "Reviewers updated",
|
||||||
@@ -67,6 +71,6 @@ func ReviewersHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
@@ -13,20 +12,27 @@ type RevisionsResponse struct {
|
|||||||
Revisions []*gitlab.MergeRequestDiffVersion
|
Revisions []*gitlab.MergeRequestDiffVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func RevisionsHandler(w http.ResponseWriter, r *http.Request) {
|
/*
|
||||||
c := r.Context().Value("client").(Client)
|
revisionsHandler gets revision information about the current MR. This data is not used directly but is
|
||||||
|
a precursor API call for other functionality
|
||||||
|
*/
|
||||||
|
func (a *api) revisionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
w.Header().Set("Allow", http.MethodGet)
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
handleError(w, InvalidRequestError{}, "Expected GET", http.StatusMethodNotAllowed)
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
versionInfo, _, err := c.git.MergeRequests.GetMergeRequestDiffVersions(c.projectId, c.mergeId, &gitlab.GetMergeRequestDiffVersionsOptions{})
|
versionInfo, res, err := a.client.GetMergeRequestDiffVersions(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.GetMergeRequestDiffVersionsOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not get diff version info", http.StatusBadRequest)
|
handleError(w, err, "Could not get diff version info", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
handleError(w, GenericError{endpoint: "/mr/revisions"}, "Could not get diff version info", res.StatusCode)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -40,7 +46,7 @@ func RevisionsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,37 +2,38 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RevokeHandler(w http.ResponseWriter, r *http.Request) {
|
/* revokeHandler revokes approval for the current merge request */
|
||||||
c := r.Context().Value("client").(Client)
|
func (a *api) revokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
w.Header().Set("Allow", http.MethodPost)
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodPost)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := c.git.MergeRequestApprovals.UnapproveMergeRequest(c.projectId, c.mergeId, nil, nil)
|
res, err := a.client.UnapproveMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, nil, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not revoke approval", http.StatusBadRequest)
|
handleError(w, err, "Could not revoke approval", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Check for non-200 status codes */
|
if res.StatusCode >= 300 {
|
||||||
w.WriteHeader(res.StatusCode)
|
handleError(w, GenericError{endpoint: "/revoke"}, "Could not revoke approval", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
response := SuccessResponse{
|
response := SuccessResponse{
|
||||||
Message: "Success! Revoked MR approval.",
|
Message: "Success! Revoked MR approval",
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
154
cmd/server.go
Normal file
154
cmd/server.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
startSever starts the server and runs concurrent goroutines
|
||||||
|
to handle potential shutdown requests and incoming HTTP requests.
|
||||||
|
*/
|
||||||
|
func startServer(client *Client, projectInfo *ProjectInfo) {
|
||||||
|
|
||||||
|
m, a := createRouterAndApi(client,
|
||||||
|
func(a *api) error {
|
||||||
|
a.projectInfo = projectInfo
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(a *api) error {
|
||||||
|
a.fileReader = attachmentReader{}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
l := createListener()
|
||||||
|
server := &http.Server{Handler: m}
|
||||||
|
|
||||||
|
/* Starts the Go server */
|
||||||
|
go func() {
|
||||||
|
err := server.Serve(l)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error starting server: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
port := l.Addr().(*net.TCPAddr).Port
|
||||||
|
err := checkServer(port)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Server did not respond: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This print is detected by the Lua code */
|
||||||
|
fmt.Println("Server started on port: ", port)
|
||||||
|
|
||||||
|
/* Handles shutdown requests */
|
||||||
|
<-a.sigCh
|
||||||
|
err = server.Shutdown(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Server could not shut down gracefully: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The api struct contains common configuration that's accessible to all handlers, such as the gitlab
|
||||||
|
client, the project information, and the channels for signaling error or shutdown requests
|
||||||
|
|
||||||
|
The handlers for different Gitlab operations are are all methods on the api struct and interact
|
||||||
|
with the client value, which is a go-gitlab client.
|
||||||
|
*/
|
||||||
|
type api struct {
|
||||||
|
client ClientInterface
|
||||||
|
projectInfo *ProjectInfo
|
||||||
|
fileReader FileReader
|
||||||
|
sigCh chan os.Signal
|
||||||
|
}
|
||||||
|
|
||||||
|
type optFunc func(a *api) error
|
||||||
|
|
||||||
|
/*
|
||||||
|
createRouterAndApi wires up the router and attaches all handlers to their respective routes. It also
|
||||||
|
iterates over all option functions to configure API fields such as the project information and default
|
||||||
|
file reader functionality
|
||||||
|
*/
|
||||||
|
func createRouterAndApi(client ClientInterface, optFuncs ...optFunc) (*http.ServeMux, api) {
|
||||||
|
m := http.NewServeMux()
|
||||||
|
a := api{
|
||||||
|
client: client,
|
||||||
|
projectInfo: &ProjectInfo{},
|
||||||
|
fileReader: nil,
|
||||||
|
sigCh: make(chan os.Signal, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mutates the API struct as necessary with configuration functions */
|
||||||
|
for _, optFunc := range optFuncs {
|
||||||
|
err := optFunc(&a)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Handle("/ping", http.HandlerFunc(pingHandler))
|
||||||
|
m.HandleFunc("/shutdown", a.shutdownHandler)
|
||||||
|
m.HandleFunc("/approve", a.approveHandler)
|
||||||
|
m.HandleFunc("/comment", a.commentHandler)
|
||||||
|
m.HandleFunc("/discussions/list", a.listDiscussionsHandler)
|
||||||
|
m.HandleFunc("/discussions/resolve", a.discussionsResolveHandler)
|
||||||
|
m.HandleFunc("/info", a.infoHandler)
|
||||||
|
m.HandleFunc("/job", a.jobHandler)
|
||||||
|
m.HandleFunc("/mr/attachment", a.attachmentHandler)
|
||||||
|
m.HandleFunc("/mr/assignee", a.assigneesHandler)
|
||||||
|
m.HandleFunc("/mr/summary", a.summaryHandler)
|
||||||
|
m.HandleFunc("/mr/reviewer", a.reviewersHandler)
|
||||||
|
m.HandleFunc("/mr/revisions", a.revisionsHandler)
|
||||||
|
m.HandleFunc("/pipeline/", a.pipelineHandler)
|
||||||
|
m.HandleFunc("/project/members", a.projectMembersHandler)
|
||||||
|
m.HandleFunc("/reply", a.replyHandler)
|
||||||
|
m.HandleFunc("/revoke", a.revokeHandler)
|
||||||
|
|
||||||
|
return m, a
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Used to check whether the server has started yet */
|
||||||
|
func pingHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprintln(w, "pong")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* checkServer pings the server repeatedly for 1 full second after startup in order to notify the plugin that the server is ready */
|
||||||
|
func checkServer(port int) error {
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
resp, err := http.Get("http://localhost:" + fmt.Sprintf("%d", port) + "/ping")
|
||||||
|
if resp.StatusCode == 200 && err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Microsecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("Could not start server!")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Creates a TCP listener on the port specified by the user or a random port */
|
||||||
|
func createListener() (l net.Listener) {
|
||||||
|
port := os.Args[2]
|
||||||
|
if port == "" {
|
||||||
|
port = "0"
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("localhost:%s", port)
|
||||||
|
l, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error starting server: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
59
cmd/shutdown.go
Normal file
59
cmd/shutdown.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type killer struct{}
|
||||||
|
|
||||||
|
func (k killer) Signal() {}
|
||||||
|
func (k killer) String() string {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShutdownRequest struct {
|
||||||
|
Restart bool `json:"restart"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* shutdownHandler will shutdown the HTTP server and exit the process by signaling to the shutdown channel */
|
||||||
|
func (a *api) shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Allow", http.MethodPost)
|
||||||
|
handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var shutdownRequest ShutdownRequest
|
||||||
|
err = json.Unmarshal(body, &shutdownRequest)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = "Shut down server"
|
||||||
|
if shutdownRequest.Restart {
|
||||||
|
text = "Restarted server"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := SuccessResponse{
|
||||||
|
Message: text,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(response)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
|
} else {
|
||||||
|
a.sigCh <- killer{}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
cmd/start.go
12
cmd/start.go
@@ -1,12 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) Start() error {
|
|
||||||
processId := os.Getpid()
|
|
||||||
fmt.Println(processId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -19,18 +18,18 @@ type SummaryUpdateResponse struct {
|
|||||||
MergeRequest *gitlab.MergeRequest `json:"mr"`
|
MergeRequest *gitlab.MergeRequest `json:"mr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func SummaryHandler(w http.ResponseWriter, r *http.Request) {
|
func (a *api) summaryHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
c := r.Context().Value("client").(Client)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if r.Method != http.MethodPut {
|
if r.Method != http.MethodPut {
|
||||||
w.Header().Set("Allow", http.MethodPut)
|
w.Header().Set("Access-Control-Allow-Methods", http.MethodPut)
|
||||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
handleError(w, InvalidRequestError{}, "Expected PUT", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,22 +38,22 @@ func SummaryHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
err = json.Unmarshal(body, &SummaryUpdateRequest)
|
err = json.Unmarshal(body, &SummaryUpdateRequest)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mr, res, err := c.git.MergeRequests.UpdateMergeRequest(c.projectId, c.mergeId, &gitlab.UpdateMergeRequestOptions{
|
mr, res, err := a.client.UpdateMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.UpdateMergeRequestOptions{
|
||||||
Description: &SummaryUpdateRequest.Description,
|
Description: &SummaryUpdateRequest.Description,
|
||||||
Title: &SummaryUpdateRequest.Title,
|
Title: &SummaryUpdateRequest.Title,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not edit merge request summary", http.StatusBadRequest)
|
handleError(w, err, "Could not edit merge request summary", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode >= 300 {
|
||||||
c.handleError(w, err, "Could not edit merge request summary", http.StatusBadRequest)
|
handleError(w, GenericError{endpoint: "/summary"}, "Could not edit merge request summary", res.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ func SummaryHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = json.NewEncoder(w).Encode(response)
|
err = json.NewEncoder(w).Encode(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
216
cmd/test.go
Normal file
216
cmd/test.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
The FakeHandlerClient is used to create a fake gitlab client for testing our handlers, where the gitlab APIs are all mocked depending on what is provided during the variable initialization, so that we can simulate different responses from Gitlab
|
||||||
|
*/
|
||||||
|
|
||||||
|
type fakeClient struct {
|
||||||
|
getMergeRequestFn func(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||||
|
updateMergeRequestFn func(pid interface{}, mergeRequest int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||||
|
unapprorveMergeRequestFn func(pid interface{}, mr int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
uploadFile func(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error)
|
||||||
|
getMergeRequestDiffVersions func(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestDiffVersionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequestDiffVersion, *gitlab.Response, error)
|
||||||
|
approveMergeRequest func(pid interface{}, mr int, opt *gitlab.ApproveMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequestApprovals, *gitlab.Response, error)
|
||||||
|
listMergeRequestDiscussions func(pid interface{}, mergeRequest int, opt *gitlab.ListMergeRequestDiscussionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Discussion, *gitlab.Response, error)
|
||||||
|
resolveMergeRequestDiscussion func(pid interface{}, mergeRequest int, discussion string, opt *gitlab.ResolveMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
||||||
|
createMergeRequestDiscussion func(pid interface{}, mergeRequest int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
||||||
|
updateMergeRequestDiscussionNote func(pid interface{}, mergeRequest int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error)
|
||||||
|
deleteMergeRequestDiscussionNote func(pid interface{}, mergeRequest int, discussion string, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
addMergeRequestDiscussionNote func(pid interface{}, mergeRequest int, discussion string, opt *gitlab.AddMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error)
|
||||||
|
listAllProjectMembers func(pid interface{}, opt *gitlab.ListProjectMembersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectMember, *gitlab.Response, error)
|
||||||
|
retryPipelineBuild func(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error)
|
||||||
|
listPipelineJobs func(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error)
|
||||||
|
getTraceFile func(pid interface{}, jobID int, options ...gitlab.RequestOptionFunc) (*bytes.Reader, *gitlab.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
State string `json:"state"`
|
||||||
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
WebURL string `json:"web_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) GetMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return f.getMergeRequestFn(pid, mergeRequest, opt, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) UpdateMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
|
||||||
|
return f.updateMergeRequestFn(pid, mergeRequest, opt, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) UnapproveMergeRequest(pid interface{}, mr int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return f.unapprorveMergeRequestFn(pid, mr, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) UploadFile(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error) {
|
||||||
|
return f.uploadFile(pid, content, filename, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) GetMergeRequestDiffVersions(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestDiffVersionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequestDiffVersion, *gitlab.Response, error) {
|
||||||
|
return f.getMergeRequestDiffVersions(pid, mergeRequest, opt, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) ApproveMergeRequest(pid interface{}, mr int, opt *gitlab.ApproveMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequestApprovals, *gitlab.Response, error) {
|
||||||
|
return f.approveMergeRequest(pid, mr, opt, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) ListMergeRequestDiscussions(pid interface{}, mergeRequest int, opt *gitlab.ListMergeRequestDiscussionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
return f.listMergeRequestDiscussions(pid, mergeRequest, opt, options...)
|
||||||
|
|
||||||
|
// now := time.Now()
|
||||||
|
// later := now.Add(time.Second * 100)
|
||||||
|
//
|
||||||
|
// discussions := []*gitlab.Discussion{
|
||||||
|
// {
|
||||||
|
// Notes: []*gitlab.Note{
|
||||||
|
// {
|
||||||
|
// CreatedAt: &now,
|
||||||
|
// Type: "DiffNote",
|
||||||
|
// Author: Author{
|
||||||
|
// Username: "hcramer",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Notes: []*gitlab.Note{
|
||||||
|
// {
|
||||||
|
// CreatedAt: &later,
|
||||||
|
// Type: "DiffNote",
|
||||||
|
// Author: Author{
|
||||||
|
// Username: "hcramer2",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// return discussions, makeResponse(200), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) ResolveMergeRequestDiscussion(pid interface{}, mergeRequest int, discussion string, opt *gitlab.ResolveMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
return f.resolveMergeRequestDiscussion(pid, mergeRequest, discussion, opt, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) CreateMergeRequestDiscussion(pid interface{}, mergeRequest int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error) {
|
||||||
|
return f.createMergeRequestDiscussion(pid, mergeRequest, opt, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) UpdateMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error) {
|
||||||
|
return f.updateMergeRequestDiscussionNote(pid, mergeRequest, discussion, note, opt, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) DeleteMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
|
||||||
|
return f.deleteMergeRequestDiscussionNote(pid, mergeRequest, discussion, note, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) AddMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, opt *gitlab.AddMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error) {
|
||||||
|
return f.addMergeRequestDiscussionNote(pid, mergeRequest, discussion, opt, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) ListAllProjectMembers(pid interface{}, opt *gitlab.ListProjectMembersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectMember, *gitlab.Response, error) {
|
||||||
|
return f.listAllProjectMembers(pid, opt, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) {
|
||||||
|
return f.retryPipelineBuild(pid, pipeline, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) ListPipelineJobs(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error) {
|
||||||
|
return f.listPipelineJobs(pid, pipelineID, opts, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeClient) GetTraceFile(pid interface{}, jobID int, options ...gitlab.RequestOptionFunc) (*bytes.Reader, *gitlab.Response, error) {
|
||||||
|
return f.getTraceFile(pid, jobID, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The assert function is a helper function used to check two comparables */
|
||||||
|
func assert[T comparable](t *testing.T, got T, want T) {
|
||||||
|
t.Helper()
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("Got '%v' but wanted '%v'", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Will create a new request with the given method, endpoint and body */
|
||||||
|
func makeRequest(t *testing.T, method string, endpoint string, body any) *http.Request {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var reader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
j, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
reader = bytes.NewReader(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest(method, endpoint, reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Serves and parses the JSON from an endpoint into the given type */
|
||||||
|
func serveRequest[T any](t *testing.T, s *http.ServeMux, request *http.Request, i T) *T {
|
||||||
|
t.Helper()
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
s.ServeHTTP(recorder, request)
|
||||||
|
result := recorder.Result()
|
||||||
|
decoder := json.NewDecoder(result.Body)
|
||||||
|
err := decoder.Decode(&i)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make response makes a simple response value with the right status code */
|
||||||
|
func makeResponse(status int) *gitlab.Response {
|
||||||
|
return &gitlab.Response{
|
||||||
|
Response: &http.Response{
|
||||||
|
StatusCode: status,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkErrorFromGitlab(t *testing.T, data ErrorResponse, msg string) {
|
||||||
|
t.Helper()
|
||||||
|
assert(t, data.Status, http.StatusInternalServerError)
|
||||||
|
assert(t, data.Message, msg)
|
||||||
|
assert(t, data.Details, "Some error from Gitlab")
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkBadMethod(t *testing.T, data ErrorResponse, methods ...string) {
|
||||||
|
t.Helper()
|
||||||
|
assert(t, data.Status, http.StatusMethodNotAllowed)
|
||||||
|
assert(t, data.Details, "Invalid request type")
|
||||||
|
expectedMethods := strings.Join(methods, " or ")
|
||||||
|
assert(t, data.Message, fmt.Sprintf("Expected %s", expectedMethods))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNon200(t *testing.T, data ErrorResponse, msg, endpoint string) {
|
||||||
|
t.Helper()
|
||||||
|
assert(t, data.Status, http.StatusSeeOther)
|
||||||
|
assert(t, data.Message, msg)
|
||||||
|
assert(t, data.Details, fmt.Sprintf("An error occurred on the %s endpoint", endpoint))
|
||||||
|
}
|
||||||
42
cmd/types.go
42
cmd/types.go
@@ -1,5 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Details string `json:"details"`
|
Details string `json:"details"`
|
||||||
@@ -10,3 +18,37 @@ type SuccessResponse struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GenericError struct {
|
||||||
|
endpoint string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e GenericError) Error() string {
|
||||||
|
return fmt.Sprintf("An error occurred on the %s endpoint", e.endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvalidRequestError struct{}
|
||||||
|
|
||||||
|
func (e InvalidRequestError) Error() string {
|
||||||
|
return "Invalid request type"
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The ClientInterface interface implements all the methods that our handlers need */
|
||||||
|
type ClientInterface interface {
|
||||||
|
GetMergeRequest(pid interface{}, mr int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||||
|
UpdateMergeRequest(pid interface{}, mr int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
|
||||||
|
UploadFile(pid interface{}, content io.Reader, filename string, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectFile, *gitlab.Response, error)
|
||||||
|
GetMergeRequestDiffVersions(pid interface{}, mr int, opt *gitlab.GetMergeRequestDiffVersionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequestDiffVersion, *gitlab.Response, error)
|
||||||
|
ApproveMergeRequest(pid interface{}, mr int, opt *gitlab.ApproveMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequestApprovals, *gitlab.Response, error)
|
||||||
|
UnapproveMergeRequest(pid interface{}, mr int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
ListMergeRequestDiscussions(pid interface{}, mergeRequest int, opt *gitlab.ListMergeRequestDiscussionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Discussion, *gitlab.Response, error)
|
||||||
|
ResolveMergeRequestDiscussion(pid interface{}, mergeRequest int, discussion string, opt *gitlab.ResolveMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
||||||
|
CreateMergeRequestDiscussion(pid interface{}, mergeRequest int, opt *gitlab.CreateMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error)
|
||||||
|
UpdateMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, opt *gitlab.UpdateMergeRequestDiscussionNoteOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Note, *gitlab.Response, error)
|
||||||
|
DeleteMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
|
||||||
|
AddMergeRequestDiscussionNote(pid interface{}, mergeRequest 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)
|
||||||
|
RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error)
|
||||||
|
ListPipelineJobs(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error)
|
||||||
|
GetTraceFile(pid interface{}, jobID int, options ...gitlab.RequestOptionFunc) (*bytes.Reader, *gitlab.Response, error)
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ local M = {
|
|||||||
---callback with data
|
---callback with data
|
||||||
---@param callback fun(data: DiscussionData): nil
|
---@param callback fun(data: DiscussionData): nil
|
||||||
M.load_discussions = function(callback)
|
M.load_discussions = function(callback)
|
||||||
job.run_job("/discussions", "POST", { blacklist = state.settings.discussion_tree.blacklist }, function(data)
|
job.run_job("/discussions/list", "POST", { blacklist = state.settings.discussion_tree.blacklist }, function(data)
|
||||||
M.discussions = data.discussions
|
M.discussions = data.discussions
|
||||||
M.unlinked_discussions = data.unlinked_discussions
|
M.unlinked_discussions = data.unlinked_discussions
|
||||||
callback(data)
|
callback(data)
|
||||||
@@ -645,7 +645,7 @@ M.toggle_discussion_resolved = function(tree)
|
|||||||
resolved = not note.resolved,
|
resolved = not note.resolved,
|
||||||
}
|
}
|
||||||
|
|
||||||
job.run_job("/discussion/resolve", "PUT", body, function(data)
|
job.run_job("/discussions/resolve", "PUT", body, function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
M.redraw_resolved_status(tree, note, not note.resolved)
|
M.redraw_resolved_status(tree, note, not note.resolved)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ M.open = function()
|
|||||||
if not pipeline then
|
if not pipeline then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local body = { pipeline_id = pipeline.id }
|
|
||||||
job.run_job("/pipeline", "GET", body, function(data)
|
job.run_job("/pipeline/" .. pipeline.id, "GET", nil, function(data)
|
||||||
local pipeline_jobs = u.reverse(type(data.Jobs) == "table" and data.Jobs or {})
|
local pipeline_jobs = u.reverse(type(data.Jobs) == "table" and data.Jobs or {})
|
||||||
M.pipeline_jobs = pipeline_jobs
|
M.pipeline_jobs = pipeline_jobs
|
||||||
|
|
||||||
@@ -92,13 +92,12 @@ M.retrigger = function()
|
|||||||
if not pipeline then
|
if not pipeline then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local body = { pipeline_id = pipeline.id }
|
|
||||||
if pipeline.status ~= "failed" then
|
if pipeline.status ~= "failed" then
|
||||||
u.notify("Pipeline is not in a failed state!", vim.log.levels.WARN)
|
u.notify("Pipeline is not in a failed state!", vim.log.levels.WARN)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
job.run_job("/pipeline", "POST", body, function()
|
job.run_job("/pipeline/" .. pipeline.id, "POST", nil, function()
|
||||||
u.notify("Pipeline re-triggered!", vim.log.levels.INFO)
|
u.notify("Pipeline re-triggered!", vim.log.levels.INFO)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ function async:fetch(dependencies, i, argTable)
|
|||||||
|
|
||||||
local dependency = dependencies[i]
|
local dependency = dependencies[i]
|
||||||
|
|
||||||
-- Do not call endpoint unless refresh is required
|
-- If we have data already and refresh is not required, skip this API call
|
||||||
if state[dependency.state] ~= nil and not dependency.refresh then
|
if state[dependency.state] ~= nil and not dependency.refresh then
|
||||||
self:fetch(dependencies, i + 1, argTable)
|
self:fetch(dependencies, i + 1, argTable)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Call the API, set the data, and then call the next API
|
||||||
job.run_job(dependency.endpoint, "GET", dependency.body, function(data)
|
job.run_job(dependency.endpoint, "GET", dependency.body, function(data)
|
||||||
state[dependency.state] = data[dependency.key]
|
state[dependency.state] = data[dependency.key]
|
||||||
self:fetch(dependencies, i + 1, argTable)
|
self:fetch(dependencies, i + 1, argTable)
|
||||||
@@ -54,11 +55,13 @@ M.sequence = function(dependencies, cb)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- If go server is already running, then start fetching the values in sequence
|
||||||
if state.go_server_running then
|
if state.go_server_running then
|
||||||
handler:fetch(dependencies, 1, argTable)
|
handler:fetch(dependencies, 1, argTable)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Otherwise, start the go server and start fetching the values
|
||||||
server.start(function()
|
server.start(function()
|
||||||
state.go_server_running = true
|
state.go_server_running = true
|
||||||
handler:fetch(dependencies, 1, argTable)
|
handler:fetch(dependencies, 1, argTable)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
-- to Gitlab and returning the data
|
-- to Gitlab and returning the data
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
|
local job = require("gitlab.job")
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- Starts the Go server and call the callback provided
|
-- Starts the Go server and call the callback provided
|
||||||
@@ -60,7 +61,12 @@ M.start = function(callback)
|
|||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
on_exit = function(job_id, exit_code)
|
on_exit = function(job_id, exit_code)
|
||||||
u.notify("Golang gitlab server exited: job_id: " .. job_id .. ", exit_code: " .. exit_code, vim.log.levels.ERROR)
|
if exit_code ~= 0 then
|
||||||
|
u.notify(
|
||||||
|
"Golang gitlab server exited: job_id: " .. job_id .. ", exit_code: " .. exit_code,
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
if job_id <= 0 then
|
if job_id <= 0 then
|
||||||
@@ -95,4 +101,41 @@ M.build = function(override)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Shuts down the Go server and clears out all old gitlab.nvim state
|
||||||
|
M.shutdown = function(cb)
|
||||||
|
if not state.go_server_running then
|
||||||
|
vim.notify("The gitlab.nvim server is not running", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
job.run_job("/shutdown", "POST", { restart = false }, function(data)
|
||||||
|
state.go_server_running = false
|
||||||
|
state.clear_data()
|
||||||
|
if cb then
|
||||||
|
cb()
|
||||||
|
else
|
||||||
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Restarts the Go server and clears out all gitlab.nvim state
|
||||||
|
M.restart = function(cb)
|
||||||
|
if not state.go_server_running then
|
||||||
|
vim.notify("The gitlab.nvim server is not running", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
job.run_job("/shutdown", "POST", { restart = true }, function(data)
|
||||||
|
state.go_server_running = false
|
||||||
|
M.start(function()
|
||||||
|
state.go_server_running = true
|
||||||
|
state.clear_data()
|
||||||
|
if cb then
|
||||||
|
cb()
|
||||||
|
else
|
||||||
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -235,7 +235,21 @@ end
|
|||||||
M.dependencies = {
|
M.dependencies = {
|
||||||
info = { endpoint = "/info", key = "info", state = "INFO", refresh = false },
|
info = { endpoint = "/info", key = "info", state = "INFO", refresh = false },
|
||||||
revisions = { endpoint = "/mr/revisions", key = "Revisions", state = "MR_REVISIONS", refresh = false },
|
revisions = { endpoint = "/mr/revisions", key = "Revisions", state = "MR_REVISIONS", refresh = false },
|
||||||
project_members = { endpoint = "/members", key = "ProjectMembers", state = "PROJECT_MEMBERS", refresh = false },
|
project_members = {
|
||||||
|
endpoint = "/project/members",
|
||||||
|
key = "ProjectMembers",
|
||||||
|
state = "PROJECT_MEMBERS",
|
||||||
|
refresh = false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- This function clears out all of the previously fetched data. It's used
|
||||||
|
-- to reset the plugin state when the Go server is restarted
|
||||||
|
M.clear_data = function()
|
||||||
|
M.INFO = nil
|
||||||
|
for _, dep in ipairs(M.dependencies) do
|
||||||
|
M[dep.state] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
Reference in New Issue
Block a user