Files
gitlab.nvim/cmd/comment.go
Harrison (Harry) Cramer d9a744300e Fixes #28 (#33)
We weren't writing a 200 status code post-delete because Gitlab returns
a 204 (empty but successful) by default. I updated this logic to change
that and also to refresh the tree.

The collapsed state of the tree will be wiped out if you delete a
bottom-level discussion but that's okay for now, not a high priority
right now.
2023-08-14 23:16:18 -04:00

309 lines
8.5 KiB
Go

package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"time"
"github.com/xanzy/go-gitlab"
)
const mrVersionsUrl = "%s/api/v4/projects/%s/merge_requests/%d/versions"
type MRVersion struct {
ID int `json:"id"`
HeadCommitSHA string `json:"head_commit_sha"`
BaseCommitSHA string `json:"base_commit_sha"`
StartCommitSHA string `json:"start_commit_sha"`
CreatedAt time.Time `json:"created_at"`
MergeRequestID int `json:"merge_request_id"`
State string `json:"state"`
RealSize string `json:"real_size"`
}
type PostCommentRequest struct {
LineNumber int `json:"line_number"`
FileName string `json:"file_name"`
Comment string `json:"comment"`
}
type DeleteCommentRequest struct {
NoteId int `json:"note_id"`
DiscussionId string `json:"discussion_id"`
}
type EditCommentRequest struct {
Comment string `json:"comment"`
NoteId int `json:"note_id"`
DiscussionId string `json:"discussion_id"`
}
type CommentResponse struct {
SuccessResponse
Comment *gitlab.Note `json:"note"`
}
func CommentHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodDelete:
DeleteComment(w, r)
case http.MethodPost:
PostComment(w, r)
case http.MethodPatch:
EditComment(w, r)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func DeleteComment(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
c := r.Context().Value("client").(Client)
body, err := io.ReadAll(r.Body)
if err != nil {
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
var deleteCommentRequest DeleteCommentRequest
err = json.Unmarshal(body, &deleteCommentRequest)
if err != nil {
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
return
}
res, err := c.git.Discussions.DeleteMergeRequestDiscussionNote(c.projectId, c.mergeId, deleteCommentRequest.DiscussionId, deleteCommentRequest.NoteId)
if err != nil {
c.handleError(w, err, "Could not delete comment", res.StatusCode)
return
}
/* TODO: Check status code */
w.WriteHeader(http.StatusOK)
response := SuccessResponse{
Message: "Comment deleted succesfully",
Status: http.StatusOK,
}
json.NewEncoder(w).Encode(response)
}
func PostComment(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
c := r.Context().Value("client").(Client)
body, err := io.ReadAll(r.Body)
if err != nil {
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
var postCommentRequest PostCommentRequest
err = json.Unmarshal(body, &postCommentRequest)
if err != nil {
c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
return
}
res, err := c.PostComment(postCommentRequest)
for k, v := range res.Header {
w.Header().Set(k, v[0])
}
if err != nil {
c.handleError(w, err, "Could not post comment", res.StatusCode)
return
}
w.WriteHeader(res.StatusCode)
/* TODO: Check for bad status codes */
response := SuccessResponse{
Message: "Comment created succesfully",
Status: http.StatusOK,
}
json.NewEncoder(w).Encode(response)
}
func EditComment(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
c := r.Context().Value("client").(Client)
body, err := io.ReadAll(r.Body)
if err != nil {
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
var editCommentRequest EditCommentRequest
err = json.Unmarshal(body, &editCommentRequest)
if err != nil {
c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
return
}
options := gitlab.UpdateMergeRequestDiscussionNoteOptions{
Body: gitlab.String(editCommentRequest.Comment),
}
note, res, err := c.git.Discussions.UpdateMergeRequestDiscussionNote(c.projectId, c.mergeId, editCommentRequest.DiscussionId, editCommentRequest.NoteId, &options)
if err != nil {
c.handleError(w, err, "Could not edit comment", res.StatusCode)
return
}
w.WriteHeader(res.StatusCode)
if res.StatusCode != http.StatusOK {
c.handleError(w, errors.New("Non-200 status code recieved"), "Could not edit comment", res.StatusCode)
}
response := CommentResponse{
SuccessResponse: SuccessResponse{
Message: "Comment edited succesfully",
Status: http.StatusOK,
},
Comment: note,
}
json.NewEncoder(w).Encode(response)
}
func (c *Client) PostComment(cr PostCommentRequest) (*http.Response, error) {
err, response := getMRVersions(c.gitlabInstance, c.projectId, c.mergeId, c.authToken)
if err != nil {
return nil, fmt.Errorf("Error making diff thread: %e", err)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
var diffVersionInfo []MRVersion
err = json.Unmarshal(body, &diffVersionInfo)
if err != nil {
return nil, fmt.Errorf("Error unmarshalling version info JSON: %w", err)
}
/* This is necessary since we do not know whether the comment is on a line that
has been changed or not. Making all three of these API calls will let us leave
the comment regardless. I ran these in sequence vai a Sync.WaitGroup, but
it was successfully posting a comment to a modified twice, so now I'm running
them in sequence.
To clean this up we might try to detect more information about the change in our
Lua code and pass it to the Go code.
See the Gitlab documentation: https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff */
for i := 0; i < 3; i++ {
ii := i
res, err := c.CommentOnDeletion(cr.LineNumber, cr.FileName, cr.Comment, diffVersionInfo[0], ii)
if err == nil && res.StatusCode >= 200 && res.StatusCode <= 299 {
return res, nil
}
}
return nil, fmt.Errorf("Could not leave comment")
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}
/* Gets the latest merge request revision data */
func getMRVersions(gitlabInstance string, projectId string, mergeId int, authToken string) (e error, response *http.Response) {
url := fmt.Sprintf(mrVersionsUrl, gitlabInstance, projectId, mergeId)
req, err := http.NewRequest(http.MethodGet, url, nil)
req.Header.Add("PRIVATE-TOKEN", authToken)
if err != nil {
return err, nil
}
client := &http.Client{}
response, err = client.Do(req)
if err != nil {
return err, nil
}
if response.StatusCode != 200 {
return errors.New("Non-200 status code: " + response.Status), nil
}
return nil, response
}
/*
Creates a new merge request discussion https://docs.gitlab.com/ee/api/discussions.html#create-new-merge-request-thread
The go-gitlab client was not working for this API specifically 😢
*/
func (c *Client) CommentOnDeletion(lineNumber int, fileName string, comment string, diffVersionInfo MRVersion, i int) (*http.Response, error) {
deletionDiscussionUrl := fmt.Sprintf(c.gitlabInstance+"/api/v4/projects/%s/merge_requests/%d/discussions", c.projectId, c.mergeId)
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
_ = writer.WriteField("body", comment)
_ = writer.WriteField("position[base_sha]", diffVersionInfo.BaseCommitSHA)
_ = writer.WriteField("position[start_sha]", diffVersionInfo.StartCommitSHA)
_ = writer.WriteField("position[head_sha]", diffVersionInfo.HeadCommitSHA)
_ = writer.WriteField("position[position_type]", "text")
/* We need to set these properties differently depending on whether we're commenting on a deleted line,
a modified line, an added line, or an unmodified line */
_ = writer.WriteField("position[old_path]", fileName)
_ = writer.WriteField("position[new_path]", fileName)
if i == 0 {
_ = writer.WriteField("position[old_line]", fmt.Sprintf("%d", lineNumber))
} else if i == 1 {
_ = writer.WriteField("position[new_line]", fmt.Sprintf("%d", lineNumber))
} else {
_ = writer.WriteField("position[old_line]", fmt.Sprintf("%d", lineNumber))
_ = writer.WriteField("position[new_line]", fmt.Sprintf("%d", lineNumber))
}
err := writer.Close()
if err != nil {
return nil, fmt.Errorf("Error making form data: %w", err)
}
client := &http.Client{}
req, err := http.NewRequest(http.MethodPost, deletionDiscussionUrl, payload)
if err != nil {
return nil, fmt.Errorf("Error building request: %w", err)
}
req.Header.Add("PRIVATE-TOKEN", c.authToken)
req.Header.Set("Content-Type", writer.FormDataContentType())
res, err := client.Do(req)
return res, err
}