Feat: Create Merge Request (#149)

- Adds the ability to create MRs to the plugin
- Adds the ability to jump to specific discussions/notes in the browser
- Fixes stale icons
- Adds debug keybinding for discussion tree for developers
This commit is contained in:
Harrison (Harry) Cramer
2023-12-19 13:41:07 -05:00
committed by GitHub
parent 35f0bc16a5
commit 37a53842d0
38 changed files with 814 additions and 204 deletions

View File

@@ -22,7 +22,7 @@ func (a *api) approveHandler(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/approve"}, "Could not approve merge request", res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/approve"}, "Could not approve merge request", res.StatusCode)
return
}

View File

@@ -22,7 +22,7 @@ func approveMergeRequestErr(pid interface{}, mr int, opt *gitlab.ApproveMergeReq
func TestApproveHandler(t *testing.T) {
t.Run("Approves merge request", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/approve", nil)
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequest})
data := serveRequest(t, server, request, SuccessResponse{})
assert(t, data.Message, "Approved MR")
@@ -30,23 +30,23 @@ func TestApproveHandler(t *testing.T) {
})
t.Run("Disallows non-POST method", func(t *testing.T) {
request := makeRequest(t, http.MethodPut, "/approve", nil)
request := makeRequest(t, http.MethodPut, "/mr/approve", nil)
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequest})
data := serveRequest(t, server, request, ErrorResponse{})
checkBadMethod(t, *data, http.MethodPost)
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/approve", nil)
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequestErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not approve merge request")
})
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/approve", nil)
request := makeRequest(t, http.MethodPost, "/mr/approve", nil)
server, _ := createRouterAndApi(fakeClient{approveMergeRequest: approveMergeRequestNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not approve merge request", "/approve")
checkNon200(t, *data, "Could not approve merge request", "/mr/approve")
})
}

View File

@@ -83,7 +83,7 @@ func (a *api) attachmentHandler(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/mr/attachment"}, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FileName), res.StatusCode)
handleError(w, GenericError{endpoint: "/attachment"}, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FileName), res.StatusCode)
return
}

View File

@@ -36,7 +36,7 @@ func withMockFileReader(a *api) error {
func TestAttachmentHandler(t *testing.T) {
t.Run("Returns 200-status response after upload", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
request := makeRequest(t, http.MethodPost, "/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFile}, withMockFileReader)
data := serveRequest(t, router, request, AttachmentResponse{})
assert(t, data.SuccessResponse.Status, http.StatusOK)
@@ -44,23 +44,23 @@ func TestAttachmentHandler(t *testing.T) {
})
t.Run("Disallows non-POST method", func(t *testing.T) {
request := makeRequest(t, http.MethodPut, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
request := makeRequest(t, http.MethodPut, "/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFile}, withMockFileReader)
data := serveRequest(t, router, request, ErrorResponse{})
checkBadMethod(t, *data, http.MethodPost)
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
request := makeRequest(t, http.MethodPost, "/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFileErr}, withMockFileReader)
data := serveRequest(t, router, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not upload some_file_name to Gitlab")
})
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
request := makeRequest(t, http.MethodPost, "/attachment", AttachmentRequest{FilePath: "some_file_path", FileName: "some_file_name"})
router, _ := createRouterAndApi(fakeClient{uploadFile: uploadFileNon200}, withMockFileReader)
data := serveRequest(t, router, request, ErrorResponse{})
checkNon200(t, *data, "Could not upload some_file_name to Gitlab", "/mr/attachment")
checkNon200(t, *data, "Could not upload some_file_name to Gitlab", "/attachment")
})
}

View File

@@ -8,7 +8,6 @@ import (
"net/http"
"net/http/httputil"
"os"
"strconv"
"github.com/hashicorp/go-retryablehttp"
"github.com/xanzy/go-gitlab"
@@ -90,7 +89,7 @@ func initGitlabClient() (error, *Client) {
}
}
/* initProjectSettings fetch the project ID and merge request ID using the client. */
/* initProjectSettings fetch the project ID using the client */
func initProjectSettings(c *Client, gitInfo GitProjectInfo) (error, *ProjectInfo) {
opt := gitlab.GetProjectOptions{}
@@ -109,31 +108,10 @@ func initProjectSettings(c *Client, gitInfo GitProjectInfo) (error, *ProjectInfo
projectId := fmt.Sprint(project.ID)
options := gitlab.ListProjectMergeRequestsOptions{
Scope: gitlab.String("all"),
State: gitlab.String("opened"),
SourceBranch: &gitInfo.BranchName,
}
mergeRequests, _, err := c.ListProjectMergeRequests(projectId, &options)
if err != nil {
return fmt.Errorf("Failed to list merge requests: %w", err), nil
}
if len(mergeRequests) == 0 {
return errors.New("No merge requests found"), nil
}
mergeId := strconv.Itoa(mergeRequests[0].IID)
mergeIdInt, err := strconv.Atoi(mergeId)
if err != nil {
return err, nil
}
return nil, &ProjectInfo{
MergeId: mergeIdInt,
ProjectId: projectId,
}
}
/* handleError is a utililty handler that returns errors to the client along with their statuses and messages */

View File

@@ -95,7 +95,7 @@ func (a *api) deleteComment(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/comment"}, "Could not delete comment", res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/comment"}, "Could not delete comment", res.StatusCode)
return
}
@@ -186,7 +186,7 @@ func (a *api) postComment(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/comment"}, "Could not create discussion", res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/comment"}, "Could not create discussion", res.StatusCode)
return
}
@@ -236,7 +236,7 @@ func (a *api) editComment(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/comment"}, "Could not update comment", res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/comment"}, "Could not update comment", res.StatusCode)
return
}

View File

@@ -22,7 +22,7 @@ func createMergeRequestDiscussionErr(pid interface{}, mergeRequest int, opt *git
func TestPostComment(t *testing.T) {
t.Run("Creates a new note (unlinked comment)", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{})
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{})
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
data := serveRequest(t, server, request, CommentResponse{})
assert(t, data.SuccessResponse.Message, "Note created successfully")
@@ -30,7 +30,7 @@ func TestPostComment(t *testing.T) {
})
t.Run("Creates a new comment", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{FileName: "some_file.txt"})
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{FileName: "some_file.txt"})
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussion})
data := serveRequest(t, server, request, CommentResponse{})
assert(t, data.SuccessResponse.Message, "Comment created successfully")
@@ -38,7 +38,7 @@ func TestPostComment(t *testing.T) {
})
t.Run("Creates a new multiline comment", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{
FileName: "some_file.txt",
LineRange: &LineRange{
StartRange: &LinePosition{}, /* These would have real data */
@@ -52,17 +52,17 @@ func TestPostComment(t *testing.T) {
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{})
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{})
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussionErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not create discussion")
})
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/comment", PostCommentRequest{})
request := makeRequest(t, http.MethodPost, "/mr/comment", PostCommentRequest{})
server, _ := createRouterAndApi(fakeClient{createMergeRequestDiscussion: createMergeRequestDiscussionNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not create discussion", "/comment")
checkNon200(t, *data, "Could not create discussion", "/mr/comment")
})
}
@@ -80,7 +80,7 @@ func deleteMergeRequestDiscussionNoteNon200(pid interface{}, mergeRequest int, d
func TestDeleteComment(t *testing.T) {
t.Run("Deletes a comment", func(t *testing.T) {
request := makeRequest(t, http.MethodDelete, "/comment", DeleteCommentRequest{})
request := makeRequest(t, http.MethodDelete, "/mr/comment", DeleteCommentRequest{})
server, _ := createRouterAndApi(fakeClient{deleteMergeRequestDiscussionNote: deleteMergeRequestDiscussionNote})
data := serveRequest(t, server, request, CommentResponse{})
assert(t, data.SuccessResponse.Message, "Comment deleted successfully")
@@ -88,17 +88,17 @@ func TestDeleteComment(t *testing.T) {
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodDelete, "/comment", DeleteCommentRequest{})
request := makeRequest(t, http.MethodDelete, "/mr/comment", DeleteCommentRequest{})
server, _ := createRouterAndApi(fakeClient{deleteMergeRequestDiscussionNote: deleteMergeRequestDiscussionNoteErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not delete comment")
})
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
request := makeRequest(t, http.MethodDelete, "/comment", DeleteCommentRequest{})
request := makeRequest(t, http.MethodDelete, "/mr/comment", DeleteCommentRequest{})
server, _ := createRouterAndApi(fakeClient{deleteMergeRequestDiscussionNote: deleteMergeRequestDiscussionNoteNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not delete comment", "/comment")
checkNon200(t, *data, "Could not delete comment", "/mr/comment")
})
}
@@ -116,7 +116,7 @@ func updateMergeRequestDiscussionNoteNon200(pid interface{}, mergeRequest int, d
func TestEditComment(t *testing.T) {
t.Run("Edits a comment", func(t *testing.T) {
request := makeRequest(t, http.MethodPatch, "/comment", EditCommentRequest{})
request := makeRequest(t, http.MethodPatch, "/mr/comment", EditCommentRequest{})
server, _ := createRouterAndApi(fakeClient{updateMergeRequestDiscussionNote: updateMergeRequestDiscussionNote})
data := serveRequest(t, server, request, CommentResponse{})
assert(t, data.SuccessResponse.Message, "Comment updated successfully")
@@ -124,16 +124,16 @@ func TestEditComment(t *testing.T) {
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPatch, "/comment", EditCommentRequest{})
request := makeRequest(t, http.MethodPatch, "/mr/comment", EditCommentRequest{})
server, _ := createRouterAndApi(fakeClient{updateMergeRequestDiscussionNote: updateMergeRequestDiscussionNoteErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not update comment")
})
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
request := makeRequest(t, http.MethodPatch, "/comment", EditCommentRequest{})
request := makeRequest(t, http.MethodPatch, "/mr/comment", EditCommentRequest{})
server, _ := createRouterAndApi(fakeClient{updateMergeRequestDiscussionNote: updateMergeRequestDiscussionNoteNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not update comment", "/comment")
checkNon200(t, *data, "Could not update comment", "/mr/comment")
})
}

81
cmd/create_mr.go Normal file
View File

@@ -0,0 +1,81 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"github.com/xanzy/go-gitlab"
)
type CreateMrRequest struct {
Title string `json:"title"`
Description string `json:"description"`
TargetBranch string `json:"target_branch"`
}
/* createMr creates a merge request */
func (a *api) createMr(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
if r.Method != http.MethodPost {
handleError(w, InvalidRequestError{}, "Expected POST", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
handleError(w, err, "Could not read request body", http.StatusBadRequest)
return
}
var createMrRequest CreateMrRequest
err = json.Unmarshal(body, &createMrRequest)
if err != nil {
handleError(w, err, "Could not unmarshal request body", http.StatusBadRequest)
return
}
if createMrRequest.Title == "" {
handleError(w, errors.New("Title cannot be empty"), "Could not create MR", http.StatusBadRequest)
return
}
if createMrRequest.TargetBranch == "" {
handleError(w, errors.New("Target branch cannot be empty"), "Could not create MR", http.StatusBadRequest)
return
}
opts := gitlab.CreateMergeRequestOptions{
Title: &createMrRequest.Title,
Description: &createMrRequest.Description,
TargetBranch: &createMrRequest.TargetBranch,
SourceBranch: &a.gitInfo.BranchName,
}
_, res, err := a.client.CreateMergeRequest(a.projectInfo.ProjectId, &opts)
if err != nil {
handleError(w, err, "Could not create MR", http.StatusInternalServerError)
return
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/create_mr"}, "Could not create MR", res.StatusCode)
return
}
response := SuccessResponse{
Status: http.StatusOK,
Message: fmt.Sprintf("MR '%s' created", createMrRequest.Title),
}
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(response)
if err != nil {
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
}
}

97
cmd/create_mr_test.go Normal file
View File

@@ -0,0 +1,97 @@
package main
import (
"errors"
"net/http"
"testing"
"github.com/xanzy/go-gitlab"
)
func createMrFn(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return &gitlab.MergeRequest{}, makeResponse(http.StatusOK), nil
}
func createMrFnErr(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return nil, nil, errors.New("Some error from Gitlab")
}
func createMrFnNon200(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return nil, makeResponse(http.StatusSeeOther), nil
}
func TestCreateMr(t *testing.T) {
t.Run("Creates an MR", func(t *testing.T) {
body := CreateMrRequest{
Title: "Some title",
Description: "Some description",
TargetBranch: "main",
}
request := makeRequest(t, http.MethodPost, "/create_mr", body)
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFn})
data := serveRequest(t, server, request, SuccessResponse{})
assert(t, data.Message, "MR 'Some title' created")
assert(t, data.Status, http.StatusOK)
})
t.Run("Disallows non-POST methods", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/create_mr", CreateMrRequest{})
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFn})
data := serveRequest(t, server, request, ErrorResponse{})
checkBadMethod(t, *data, http.MethodPost)
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
body := CreateMrRequest{
Title: "Some title",
Description: "Some description",
TargetBranch: "main",
}
request := makeRequest(t, http.MethodPost, "/create_mr", body)
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFnErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not create MR")
})
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
body := CreateMrRequest{
Title: "Some title",
Description: "Some description",
TargetBranch: "main",
}
request := makeRequest(t, http.MethodPost, "/create_mr", body)
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFnNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not create MR", "/create_mr")
})
t.Run("Handles missing titles", func(t *testing.T) {
body := CreateMrRequest{
Title: "",
Description: "Some description",
TargetBranch: "main",
}
request := makeRequest(t, http.MethodPost, "/create_mr", body)
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFn})
data := serveRequest(t, server, request, ErrorResponse{})
assert(t, data.Status, http.StatusBadRequest)
assert(t, data.Message, "Could not create MR")
assert(t, data.Details, "Title cannot be empty")
})
t.Run("Handles missing target branch", func(t *testing.T) {
body := CreateMrRequest{
Title: "Some title",
Description: "Some description",
TargetBranch: "",
}
request := makeRequest(t, http.MethodPost, "/create_mr", body)
server, _ := createRouterAndApi(fakeClient{createMrFn: createMrFn})
data := serveRequest(t, server, request, ErrorResponse{})
assert(t, data.Status, http.StatusBadRequest)
assert(t, data.Message, "Could not create MR")
assert(t, data.Details, "Target branch cannot be empty")
})
}

View File

@@ -28,7 +28,7 @@ func (a *api) infoHandler(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/info"}, "Could not get project info", res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/info"}, "Could not get project info", res.StatusCode)
return
}

View File

@@ -22,7 +22,7 @@ func getInfoErr(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsO
func TestInfoHandler(t *testing.T) {
t.Run("Returns normal information", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/info", nil)
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfo})
data := serveRequest(t, server, request, InfoResponse{})
assert(t, data.Info.Title, "Some Title")
@@ -31,23 +31,23 @@ func TestInfoHandler(t *testing.T) {
})
t.Run("Disallows non-GET method", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/info", nil)
request := makeRequest(t, http.MethodPost, "/mr/info", nil)
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfo})
data := serveRequest(t, server, request, ErrorResponse{})
checkBadMethod(t, *data, http.MethodGet)
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/info", nil)
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfoErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not get project info")
})
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/info", nil)
request := makeRequest(t, http.MethodGet, "/mr/info", nil)
server, _ := createRouterAndApi(fakeClient{getMergeRequestFn: getInfoNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not get project info", "/info")
checkNon200(t, *data, "Could not get project info", "/mr/info")
})
}

View File

@@ -75,7 +75,7 @@ func (a *api) listDiscussionsHandler(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/discussions/list"}, "Could not list discussions", res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/discussions/list"}, "Could not list discussions", res.StatusCode)
return
}

View File

@@ -49,7 +49,7 @@ func listMergeRequestDiscussionsNon200(pid interface{}, mergeRequest int, opt *g
func TestListDiscussionsHandler(t *testing.T) {
t.Run("Returns sorted discussions", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{})
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussions})
data := serveRequest(t, server, request, DiscussionsResponse{})
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
@@ -59,7 +59,7 @@ func TestListDiscussionsHandler(t *testing.T) {
})
t.Run("Uses blacklist to filter unwanted authors", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer"}})
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{"hcramer"}})
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussions})
data := serveRequest(t, server, request, DiscussionsResponse{})
assert(t, data.SuccessResponse.Message, "Discussions retrieved")
@@ -69,23 +69,23 @@ func TestListDiscussionsHandler(t *testing.T) {
})
t.Run("Disallows non-POST method", func(t *testing.T) {
request := makeRequest(t, http.MethodPatch, "/discussions/list", DiscussionsRequest{})
request := makeRequest(t, http.MethodPatch, "/mr/discussions/list", DiscussionsRequest{})
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussions})
data := serveRequest(t, server, request, ErrorResponse{})
checkBadMethod(t, *data, http.MethodPost)
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{})
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussionsErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not list discussions")
})
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/discussions/list", DiscussionsRequest{})
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{})
server, _ := createRouterAndApi(fakeClient{listMergeRequestDiscussions: listMergeRequestDiscussionsNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not list discussions", "/discussions/list")
checkNon200(t, *data, "Could not list discussions", "/mr/discussions/list")
})
}

View File

@@ -20,5 +20,5 @@ func main() {
log.Fatalf("Failed to initialize project settings: %v", err)
}
startServer(client, projectInfo)
startServer(client, projectInfo, gitInfo)
}

View File

@@ -53,7 +53,7 @@ func (a *api) acceptAndMergeHandler(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/merge"}, "Could not merge MR", res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/merge"}, "Could not merge MR", res.StatusCode)
return
}

View File

@@ -22,7 +22,7 @@ func acceptAndMergeNon200(pid interface{}, mergeRequest int, opt *gitlab.AcceptM
func TestAcceptAndMergeHandler(t *testing.T) {
t.Run("Accepts and merges a merge request", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/merge", AcceptMergeRequestRequest{})
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFn})
data := serveRequest(t, server, request, SuccessResponse{})
assert(t, data.Message, "MR merged successfully")
@@ -30,23 +30,23 @@ func TestAcceptAndMergeHandler(t *testing.T) {
})
t.Run("Disallows non-POST methods", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/merge", AcceptMergeRequestRequest{})
request := makeRequest(t, http.MethodGet, "/mr/merge", AcceptMergeRequestRequest{})
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFn})
data := serveRequest(t, server, request, ErrorResponse{})
checkBadMethod(t, *data, http.MethodPost)
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/merge", AcceptMergeRequestRequest{})
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeFnErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not merge MR")
})
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/merge", AcceptMergeRequestRequest{})
request := makeRequest(t, http.MethodPost, "/mr/merge", AcceptMergeRequestRequest{})
server, _ := createRouterAndApi(fakeClient{acceptAndMergeFn: acceptAndMergeNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not merge MR", "/merge")
checkNon200(t, *data, "Could not merge MR", "/mr/merge")
})
}

View File

@@ -57,7 +57,7 @@ func (a *api) replyHandler(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/reply"}, "Could not leave reply", res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/reply"}, "Could not leave reply", res.StatusCode)
return
}

View File

@@ -22,7 +22,7 @@ func addMergeRequestDiscussionNoteNon200(pid interface{}, mergeRequest int, disc
func TestReplyHandler(t *testing.T) {
t.Run("Sends a reply", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/reply", ReplyRequest{})
request := makeRequest(t, http.MethodPost, "/mr/reply", ReplyRequest{})
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNote})
data := serveRequest(t, server, request, ReplyResponse{})
assert(t, data.SuccessResponse.Message, "Replied to comment")
@@ -30,23 +30,23 @@ func TestReplyHandler(t *testing.T) {
})
t.Run("Disallows non-POST methods", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/reply", ReplyRequest{})
request := makeRequest(t, http.MethodGet, "/mr/reply", ReplyRequest{})
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNote})
data := serveRequest(t, server, request, ErrorResponse{})
checkBadMethod(t, *data, http.MethodPost)
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/reply", ReplyRequest{})
request := makeRequest(t, http.MethodPost, "/mr/reply", ReplyRequest{})
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNoteErr})
data := serveRequest(t, server, request, ErrorResponse{})
checkErrorFromGitlab(t, *data, "Could not leave reply")
})
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/reply", ReplyRequest{})
request := makeRequest(t, http.MethodPost, "/mr/reply", ReplyRequest{})
server, _ := createRouterAndApi(fakeClient{addMergeRequestDiscussionNote: addMergeRequestDiscussionNoteNon200})
data := serveRequest(t, server, request, ErrorResponse{})
checkNon200(t, *data, "Could not leave reply", "/reply")
checkNon200(t, *data, "Could not leave reply", "/mr/reply")
})
}

View File

@@ -57,7 +57,7 @@ func (a *api) discussionsResolveHandler(w http.ResponseWriter, r *http.Request)
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/discussions/resolve"}, fmt.Sprintf("Could not %s discussion", friendlyName), res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/discussions/resolve"}, fmt.Sprintf("Could not %s discussion", friendlyName), res.StatusCode)
return
}

View File

@@ -22,7 +22,7 @@ func (a *api) revokeHandler(w http.ResponseWriter, r *http.Request) {
}
if res.StatusCode >= 300 {
handleError(w, GenericError{endpoint: "/revoke"}, "Could not revoke approval", res.StatusCode)
handleError(w, GenericError{endpoint: "/mr/revoke"}, "Could not revoke approval", res.StatusCode)
return
}

View File

@@ -7,14 +7,17 @@ import (
"net"
"net/http"
"os"
"strconv"
"time"
"github.com/xanzy/go-gitlab"
)
/*
startSever starts the server and runs concurrent goroutines
to handle potential shutdown requests and incoming HTTP requests.
*/
func startServer(client *Client, projectInfo *ProjectInfo) {
func startServer(client *Client, projectInfo *ProjectInfo, gitInfo GitProjectInfo) {
m, a := createRouterAndApi(client,
func(a *api) error {
@@ -24,6 +27,10 @@ func startServer(client *Client, projectInfo *ProjectInfo) {
func(a *api) error {
a.fileReader = attachmentReader{}
return nil
},
func(a *api) error {
a.gitInfo = &gitInfo
return nil
})
l := createListener()
@@ -73,6 +80,7 @@ with the client value, which is a go-gitlab client.
type api struct {
client ClientInterface
projectInfo *ProjectInfo
gitInfo *GitProjectInfo
fileReader FileReader
sigCh chan os.Signal
}
@@ -84,11 +92,13 @@ createRouterAndApi wires up the router and attaches all handlers to their respec
iterates over all option functions to configure API fields such as the project information and default
file reader functionality
*/
func createRouterAndApi(client ClientInterface, optFuncs ...optFunc) (*http.ServeMux, api) {
m := http.NewServeMux()
a := api{
client: client,
projectInfo: &ProjectInfo{},
gitInfo: &GitProjectInfo{},
fileReader: nil,
sigCh: make(chan os.Signal, 1),
}
@@ -101,24 +111,27 @@ func createRouterAndApi(client ClientInterface, optFuncs ...optFunc) (*http.Serv
}
}
m.Handle("/ping", http.HandlerFunc(pingHandler))
m.HandleFunc("/shutdown", a.shutdownHandler)
m.HandleFunc("/approve", a.approveHandler)
m.HandleFunc("/comment", a.commentHandler)
m.HandleFunc("/merge", a.acceptAndMergeHandler)
m.HandleFunc("/discussions/list", a.listDiscussionsHandler)
m.HandleFunc("/discussions/resolve", a.discussionsResolveHandler)
m.HandleFunc("/info", a.infoHandler)
m.HandleFunc("/mr/approve", a.withMr(a.approveHandler))
m.HandleFunc("/mr/comment", a.withMr(a.commentHandler))
m.HandleFunc("/mr/merge", a.withMr(a.acceptAndMergeHandler))
m.HandleFunc("/mr/discussions/list", a.withMr(a.listDiscussionsHandler))
m.HandleFunc("/mr/discussions/resolve", a.withMr(a.discussionsResolveHandler))
m.HandleFunc("/mr/info", a.withMr(a.infoHandler))
m.HandleFunc("/mr/assignee", a.withMr(a.assigneesHandler))
m.HandleFunc("/mr/summary", a.withMr(a.summaryHandler))
m.HandleFunc("/mr/reviewer", a.withMr(a.reviewersHandler))
m.HandleFunc("/mr/revisions", a.withMr(a.revisionsHandler))
m.HandleFunc("/mr/reply", a.withMr(a.replyHandler))
m.HandleFunc("/mr/revoke", a.withMr(a.revokeHandler))
m.HandleFunc("/attachment", a.attachmentHandler)
m.HandleFunc("/create_mr", a.createMr)
m.HandleFunc("/job", a.jobHandler)
m.HandleFunc("/mr/attachment", a.attachmentHandler)
m.HandleFunc("/mr/assignee", a.assigneesHandler)
m.HandleFunc("/mr/summary", a.summaryHandler)
m.HandleFunc("/mr/reviewer", a.reviewersHandler)
m.HandleFunc("/mr/revisions", a.revisionsHandler)
m.HandleFunc("/pipeline/", a.pipelineHandler)
m.HandleFunc("/project/members", a.projectMembersHandler)
m.HandleFunc("/reply", a.replyHandler)
m.HandleFunc("/revoke", a.revokeHandler)
m.HandleFunc("/shutdown", a.shutdownHandler)
m.Handle("/ping", http.HandlerFunc(pingHandler))
return m, a
}
@@ -157,3 +170,41 @@ func createListener() (l net.Listener) {
return l
}
/* withMr is a Middlware that gets the current merge request ID and attaches it to the projectInfo */
func (a *api) withMr(f func(w http.ResponseWriter, r *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if a.projectInfo.MergeId != 0 {
f(w, r)
return
}
options := gitlab.ListProjectMergeRequestsOptions{
Scope: gitlab.String("all"),
State: gitlab.String("opened"),
SourceBranch: &a.gitInfo.BranchName,
}
mergeRequests, _, err := a.client.ListProjectMergeRequests(a.projectInfo.ProjectId, &options)
if err != nil {
handleError(w, fmt.Errorf("Failed to list merge requests: %w", err), "Failed to list merge requests", http.StatusInternalServerError)
return
}
if len(mergeRequests) == 0 {
handleError(w, fmt.Errorf("No merge requests found for branch '%s'", a.gitInfo.BranchName), "No merge requests found", http.StatusBadRequest)
return
}
mergeId := strconv.Itoa(mergeRequests[0].IID)
mergeIdInt, err := strconv.Atoi(mergeId)
if err != nil {
handleError(w, err, "Could not convert merge ID to integer", http.StatusBadRequest)
return
}
a.projectInfo.MergeId = mergeIdInt
f(w, r)
}
}

View File

@@ -18,6 +18,7 @@ The FakeHandlerClient is used to create a fake gitlab client for testing our han
*/
type fakeClient struct {
createMrFn func(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
getMergeRequestFn func(pid interface{}, mergeRequest int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
updateMergeRequestFn func(pid interface{}, mergeRequest int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
acceptAndMergeFn func(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
@@ -47,6 +48,10 @@ type Author struct {
WebURL string `json:"web_url"`
}
func (f fakeClient) CreateMergeRequest(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return f.createMrFn(pid, opt, options...)
}
func (f fakeClient) AcceptMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error) {
return f.acceptAndMergeFn(pid, mergeRequest, opt, options...)
}
@@ -77,35 +82,6 @@ func (f fakeClient) ApproveMergeRequest(pid interface{}, mr int, opt *gitlab.App
func (f fakeClient) ListMergeRequestDiscussions(pid interface{}, mergeRequest int, opt *gitlab.ListMergeRequestDiscussionsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Discussion, *gitlab.Response, error) {
return f.listMergeRequestDiscussions(pid, mergeRequest, opt, options...)
// now := time.Now()
// later := now.Add(time.Second * 100)
//
// discussions := []*gitlab.Discussion{
// {
// Notes: []*gitlab.Note{
// {
// CreatedAt: &now,
// Type: "DiffNote",
// Author: Author{
// Username: "hcramer",
// },
// },
// },
// },
// {
// Notes: []*gitlab.Note{
// {
// CreatedAt: &later,
// Type: "DiffNote",
// Author: Author{
// Username: "hcramer2",
// },
// },
// },
// },
// }
// return discussions, makeResponse(200), nil
}
func (f fakeClient) ResolveMergeRequestDiscussion(pid interface{}, mergeRequest int, discussion string, opt *gitlab.ResolveMergeRequestDiscussionOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Discussion, *gitlab.Response, error) {
@@ -144,6 +120,11 @@ func (f fakeClient) GetTraceFile(pid interface{}, jobID int, options ...gitlab.R
return f.getTraceFile(pid, jobID, options...)
}
/* This middleware function needs to return an ID for the rest of the handlers */
func (f fakeClient) ListProjectMergeRequests(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error) {
return []*gitlab.MergeRequest{{ID: 1}}, &gitlab.Response{}, nil
}
/* The assert function is a helper function used to check two comparables */
func assert[T comparable](t *testing.T, got T, want T) {
t.Helper()

View File

@@ -35,6 +35,8 @@ func (e InvalidRequestError) Error() string {
/* The ClientInterface interface implements all the methods that our handlers need */
type ClientInterface interface {
CreateMergeRequest(pid interface{}, opt *gitlab.CreateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
ListProjectMergeRequests(pid interface{}, opt *gitlab.ListProjectMergeRequestsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.MergeRequest, *gitlab.Response, error)
GetMergeRequest(pid interface{}, mr int, opt *gitlab.GetMergeRequestsOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
AcceptMergeRequest(pid interface{}, mergeRequest int, opt *gitlab.AcceptMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)
UpdateMergeRequest(pid interface{}, mr int, opt *gitlab.UpdateMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.MergeRequest, *gitlab.Response, error)