From 63fc025070c1432d5c69232120f20e7524b98fbb Mon Sep 17 00:00:00 2001 From: "Harrison (Harry) Cramer" <32515581+harrisoncramer@users.noreply.github.com> Date: Fri, 19 May 2023 17:28:58 -0700 Subject: [PATCH] Change to HTTP Model (#5) --- cmd/approve.go | 40 +++++- cmd/client.go | 32 +++-- cmd/comment.go | 245 +++++++++++++++++++++++++++++-------- cmd/delete_comment.go | 29 ----- cmd/edit_comment.go | 46 ------- cmd/info.go | 35 ++++-- cmd/list_discussions.go | 48 ++++++-- cmd/main.go | 72 ++++++----- cmd/reply.go | 86 ++++++++++--- cmd/revoke.go | 39 +++++- cmd/star.go | 41 ++++++- cmd/start.go | 12 ++ cmd/types.go | 11 ++ logs | 1 + lua/gitlab/approve.lua | 18 --- lua/gitlab/comment.lua | 110 ++++++----------- lua/gitlab/discussions.lua | 133 ++++++++++---------- lua/gitlab/init.lua | 57 +++++---- lua/gitlab/job.lua | 46 +++++++ lua/gitlab/revoke.lua | 17 --- lua/gitlab/utils/init.lua | 1 - 21 files changed, 689 insertions(+), 430 deletions(-) delete mode 100644 cmd/delete_comment.go delete mode 100644 cmd/edit_comment.go create mode 100644 cmd/start.go create mode 100644 cmd/types.go create mode 100644 logs delete mode 100644 lua/gitlab/approve.lua create mode 100644 lua/gitlab/job.lua delete mode 100644 lua/gitlab/revoke.lua diff --git a/cmd/approve.go b/cmd/approve.go index d08534f..f29eeac 100644 --- a/cmd/approve.go +++ b/cmd/approve.go @@ -1,18 +1,46 @@ package main import ( + "encoding/json" "fmt" + "net/http" ) -func (c *Client) Approve() error { +func (c *Client) Approve() (string, int, error) { - _, _, err := c.git.MergeRequestApprovals.ApproveMergeRequest(c.projectId, c.mergeId, nil, nil) + _, res, err := c.git.MergeRequestApprovals.ApproveMergeRequest(c.projectId, c.mergeId, nil, nil) if err != nil { - return fmt.Errorf("Approving MR failed: %w", err) + return "", res.Response.StatusCode, fmt.Errorf("Approving MR failed: %w", err) } - fmt.Println("Success! Approved MR.") - - return nil + return "Success! Approved MR.", http.StatusOK, nil +} + +func ApproveHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + client := r.Context().Value("client").(Client) + msg, status, err := client.Approve() + w.WriteHeader(status) + + if err != nil { + response := ErrorResponse{ + Message: err.Error(), + Status: status, + } + json.NewEncoder(w).Encode(response) + return + } + + response := SuccessResponse{ + Message: msg, + Status: http.StatusOK, + } + + json.NewEncoder(w).Encode(response) } diff --git a/cmd/client.go b/cmd/client.go index e0099c5..32f9082 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -16,22 +16,37 @@ type Client struct { git *gitlab.Client } +type Logger struct { + Active bool +} + +func (l Logger) Printf(s string, args ...interface{}) { + logString := fmt.Sprintf(s+"\n", args...) + file, err := os.OpenFile("./logs", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + panic(err) + } + defer file.Close() + _, err = file.Write([]byte(logString)) +} + /* This will initialize the client with the token and check for the basic project ID and command arguments */ func (c *Client) Init(branchName string) error { - if len(os.Args) < 3 { - return errors.New("Must provide command and projectId") + if len(os.Args) < 2 { + return errors.New("Must provide project ID!") } - command, projectId := os.Args[1], os.Args[2] - c.command = command + projectId := os.Args[1] c.projectId = projectId if projectId == "" { - return errors.New("Must provide projectId") + return errors.New("Project ID cannot be empty") } - git, err := gitlab.NewClient(os.Getenv("GITLAB_TOKEN")) + var l Logger + git, err := gitlab.NewClient(os.Getenv("GITLAB_TOKEN"), gitlab.WithCustomLogger(l)) + if err != nil { return fmt.Errorf("Failed to create client: %v", err) } @@ -61,8 +76,3 @@ func (c *Client) Init(branchName string) error { return nil } - -func (c *Client) Usage(command string) { - fmt.Printf("Usage: gitlab-nvim %s ...args", command) - os.Exit(1) -} diff --git a/cmd/comment.go b/cmd/comment.go index 5908329..26a420c 100644 --- a/cmd/comment.go +++ b/cmd/comment.go @@ -5,12 +5,11 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" - "log" "mime/multipart" "net/http" "os" - "strconv" "time" "github.com/xanzy/go-gitlab" @@ -29,20 +28,192 @@ type MRVersion struct { RealSize string `json:"real_size"` } -func (c *Client) Comment() error { - if len(os.Args) < 6 { - c.Usage("comment") +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"` +} + +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 { + w.WriteHeader(http.StatusBadRequest) + errMsg := map[string]string{"message": "Could not read request body"} + jsonMsg, _ := json.Marshal(errMsg) + w.Write(jsonMsg) + return } - lineNumber, fileName, comment := os.Args[3], os.Args[4], os.Args[5] - if lineNumber == "" || fileName == "" || comment == "" { - c.Usage("comment") + defer r.Body.Close() + + var deleteCommentRequest DeleteCommentRequest + err = json.Unmarshal(body, &deleteCommentRequest) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + errMsg := map[string]string{"message": "Could not read JSON from request"} + jsonMsg, _ := json.Marshal(errMsg) + w.Write(jsonMsg) + return } + res, err := c.git.Discussions.DeleteMergeRequestDiscussionNote(c.projectId, c.mergeId, deleteCommentRequest.DiscussionId, deleteCommentRequest.NoteId) + + w.WriteHeader(res.Response.StatusCode) + + if err != nil { + response := ErrorResponse{ + Message: err.Error(), + Status: res.Response.StatusCode, + } + json.NewEncoder(w).Encode(response) + return + } + + 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 { + w.WriteHeader(http.StatusBadRequest) + errMsg := map[string]string{"message": "Could not read request body"} + jsonMsg, _ := json.Marshal(errMsg) + w.Write(jsonMsg) + return + } + + defer r.Body.Close() + + var postCommentRequest PostCommentRequest + err = json.Unmarshal(body, &postCommentRequest) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + errMsg := map[string]string{"message": "Could not unmarshal data from request body"} + jsonMsg, _ := json.Marshal(errMsg) + w.Write(jsonMsg) + return + } + + res, err := c.PostComment(postCommentRequest) + for k, v := range res.Header { + w.Header().Set(k, v[0]) + } + + if err != nil { + response := ErrorResponse{ + Message: err.Error(), + Status: res.StatusCode, + } + json.NewEncoder(w).Encode(response) + return + } + + response := SuccessResponse{ + Message: "Comment created succesfully", + Status: http.StatusOK, + } + + json.NewEncoder(w).Encode(response) + + // w.WriteHeader(res.StatusCode) + // io.Copy(w, res.Body) + +} + +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 { + w.WriteHeader(http.StatusBadRequest) + errMsg := map[string]string{"message": "Could not read request body"} + jsonMsg, _ := json.Marshal(errMsg) + w.Write(jsonMsg) + return + } + + defer r.Body.Close() + + var editCommentRequest EditCommentRequest + err = json.Unmarshal(body, &editCommentRequest) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + errMsg := map[string]string{"message": "Could not unmarshal data from request body"} + jsonMsg, _ := json.Marshal(errMsg) + w.Write(jsonMsg) + return + } + + options := gitlab.UpdateMergeRequestDiscussionNoteOptions{ + Body: gitlab.String(editCommentRequest.Comment), + } + + _, res, err := c.git.Discussions.UpdateMergeRequestDiscussionNote(c.projectId, c.mergeId, editCommentRequest.DiscussionId, editCommentRequest.NoteId, &options) + + for k, v := range res.Header { + w.Header().Set(k, v[0]) + } + + if err != nil { + response := ErrorResponse{ + Message: err.Error(), + Status: res.StatusCode, + } + json.NewEncoder(w).Encode(response) + return + } + + response := SuccessResponse{ + Message: "Comment edited succesfully", + Status: http.StatusOK, + } + + json.NewEncoder(w).Encode(response) +} + +func (c *Client) PostComment(cr PostCommentRequest) (*http.Response, error) { + err, response := getMRVersions(c.projectId, c.mergeId) if err != nil { - log.Fatalf("Error making diff thread: %s", err) + return nil, fmt.Errorf("Error making diff thread: %e", err) } + defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) @@ -50,7 +221,7 @@ func (c *Client) Comment() error { var diffVersionInfo []MRVersion err = json.Unmarshal(body, &diffVersionInfo) if err != nil { - return fmt.Errorf("Error unmarshalling version info JSON: %w", err) + 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 @@ -65,14 +236,14 @@ func (c *Client) Comment() error { 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 - _, err := c.CommentOnDeletion(lineNumber, fileName, comment, diffVersionInfo[0], ii) - if err == nil { - fmt.Println("Left Comment: " + comment[0:min(len(comment), 25)] + "...") - return nil + 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 fmt.Errorf("Could not leave comment") + return nil, fmt.Errorf("Could not leave comment") } @@ -115,7 +286,7 @@ func getMRVersions(projectId string, mergeId int) (e error, response *http.Respo 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 string, fileName string, comment string, diffVersionInfo MRVersion, i int) (*http.Response, error) { +func (c *Client) CommentOnDeletion(lineNumber int, fileName string, comment string, diffVersionInfo MRVersion, i int) (*http.Response, error) { deletionDiscussionUrl := fmt.Sprintf("https://gitlab.com/api/v4/projects/%s/merge_requests/%d/discussions", c.projectId, c.mergeId) @@ -132,12 +303,12 @@ func (c *Client) CommentOnDeletion(lineNumber string, fileName string, comment s _ = writer.WriteField("position[old_path]", fileName) _ = writer.WriteField("position[new_path]", fileName) if i == 0 { - _ = writer.WriteField("position[old_line]", lineNumber) + _ = writer.WriteField("position[old_line]", fmt.Sprintf("%d", lineNumber)) } else if i == 1 { - _ = writer.WriteField("position[new_line]", lineNumber) + _ = writer.WriteField("position[new_line]", fmt.Sprintf("%d", lineNumber)) } else { - _ = writer.WriteField("position[old_line]", lineNumber) - _ = writer.WriteField("position[new_line]", lineNumber) + _ = writer.WriteField("position[old_line]", fmt.Sprintf("%d", lineNumber)) + _ = writer.WriteField("position[new_line]", fmt.Sprintf("%d", lineNumber)) } err := writer.Close() @@ -152,39 +323,9 @@ func (c *Client) CommentOnDeletion(lineNumber string, fileName string, comment s return nil, fmt.Errorf("Error building request: %w", err) } req.Header.Add("PRIVATE-TOKEN", os.Getenv("GITLAB_TOKEN")) - req.Header.Set("Content-Type", writer.FormDataContentType()) + res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("Error making request: %w", err) - } - defer res.Body.Close() - return res, nil -} - -func (c *Client) OverviewComment() error { - lineNumber, fileName, comment, sha := os.Args[3], os.Args[4], os.Args[5], os.Args[6] - if lineNumber == "" || fileName == "" || comment == "" { - c.Usage("comment") - } - - lineNumberInt, err := strconv.Atoi(lineNumber) - if err != nil { - return fmt.Errorf("Not a valid line number: %w", err) - } - - postCommitCommentOptions := gitlab.PostCommitCommentOptions{ - Note: gitlab.String(comment), - Path: gitlab.String(fileName), - Line: &lineNumberInt, - LineType: gitlab.String("old"), - } - _, _, err = c.git.Commits.PostCommitComment(c.projectId, sha, &postCommitCommentOptions) - if err != nil { - return fmt.Errorf("Error leaving overview comment: %w", err) - } - - fmt.Println("Left Overview Comment: " + comment[0:min(len(comment), 25)] + "...") - return nil + return res, err } diff --git a/cmd/delete_comment.go b/cmd/delete_comment.go deleted file mode 100644 index 507ca19..0000000 --- a/cmd/delete_comment.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strconv" -) - -func (c *Client) DeleteComment() error { - discussionId, noteId := os.Args[3], os.Args[4] - if discussionId == "" || noteId == "" { - c.Usage("deleteComment") - } - - noteIdInt, err := strconv.Atoi(noteId) - if err != nil { - return fmt.Errorf("Could not convert noteId to int: %w", err) - } - - _, err = c.git.Discussions.DeleteMergeRequestDiscussionNote(c.projectId, c.mergeId, discussionId, noteIdInt) - - if err != nil { - return fmt.Errorf("Could not delete comment: %w", err) - } - - fmt.Println("Deleted comment") - - return nil -} diff --git a/cmd/edit_comment.go b/cmd/edit_comment.go deleted file mode 100644 index a4e20b5..0000000 --- a/cmd/edit_comment.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "strconv" - - "github.com/xanzy/go-gitlab" -) - -func (c *Client) EditComment() error { - if len(os.Args) < 6 { - return errors.New("Must provide commentId, noteId, and edited text") - } - - discussionId, noteId, newCommentText := os.Args[3], os.Args[4], os.Args[5] - if discussionId == "" || newCommentText == "" { - return errors.New("Must provide commentId, noteId, and edited text") - } - - options := gitlab.UpdateMergeRequestDiscussionNoteOptions{ - Body: gitlab.String(newCommentText), - } - - noteIdInt, err := strconv.Atoi(noteId) - if err != nil { - return errors.New("Not a valid noteId") - } - - gitlabNote, _, err := c.git.Discussions.UpdateMergeRequestDiscussionNote(c.projectId, c.mergeId, discussionId, noteIdInt, &options) - - if err != nil { - return fmt.Errorf("Failed to edit comment: %w", err) - } - - note, err := json.Marshal(gitlabNote) - if err != nil { - return fmt.Errorf("Failed to marshal note: %w", err) - } - - fmt.Println(string(note)) - - return nil -} diff --git a/cmd/info.go b/cmd/info.go index 268c0d2..98327c1 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "errors" "fmt" "io" @@ -10,13 +11,13 @@ import ( const mrUrl = "https://gitlab.com/api/v4/projects/%s/merge_requests/%d" -func (c *Client) Info() error { +func (c *Client) Info() ([]byte, error) { url := fmt.Sprintf(mrUrl, c.projectId, c.mergeId) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - return fmt.Errorf("Failed to build read request: %w", err) + return nil, fmt.Errorf("Failed to build read request: %w", err) } req.Header.Set("PRIVATE-TOKEN", os.Getenv("GITLAB_TOKEN")) @@ -25,22 +26,42 @@ func (c *Client) Info() error { res, err := http.DefaultClient.Do(req) if err != nil { - return fmt.Errorf("Failed to make info request: %w", err) + return nil, fmt.Errorf("Failed to make info request: %w", err) } defer res.Body.Close() if res.StatusCode < 200 || res.StatusCode >= 300 { - return errors.New(fmt.Sprintf("Recieved non-200 response: %d", res.StatusCode)) + return nil, errors.New(fmt.Sprintf("Recieved non-200 response: %d", res.StatusCode)) } body, err := io.ReadAll(res.Body) if err != nil { - return fmt.Errorf("Failed to parse read response: %w", err) + return nil, fmt.Errorf("Failed to parse read response: %w", err) } /* This response is parsed into a table in our Lua code */ - fmt.Println(string(body)) + return body, nil - return nil +} + +func InfoHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + client := r.Context().Value("client").(Client) + msg, err := client.Info() + if err != nil { + errResp := map[string]string{"message": err.Error()} + response, _ := json.Marshal(errResp) + w.WriteHeader(http.StatusInternalServerError) + w.Write(response) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(msg) } diff --git a/cmd/list_discussions.go b/cmd/list_discussions.go index 1cb1a70..60f63c4 100644 --- a/cmd/list_discussions.go +++ b/cmd/list_discussions.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "net/http" "sort" "encoding/json" @@ -11,6 +12,11 @@ import ( type SortableDiscussions []*gitlab.Discussion +type DiscussionsResponse struct { + SuccessResponse + Discussions []*gitlab.Discussion `json:"discussions"` +} + func (n SortableDiscussions) Len() int { return len(n) } @@ -26,16 +32,16 @@ func (n SortableDiscussions) Swap(i, j int) { n[i], n[j] = n[j], n[i] } -func (c *Client) ListDiscussions() error { +func (c *Client) ListDiscussions() ([]*gitlab.Discussion, int, error) { mergeRequestDiscussionOptions := gitlab.ListMergeRequestDiscussionsOptions{ Page: 1, PerPage: 250, } - discussions, _, err := c.git.Discussions.ListMergeRequestDiscussions(c.projectId, c.mergeId, &mergeRequestDiscussionOptions, nil) + discussions, res, err := c.git.Discussions.ListMergeRequestDiscussions(c.projectId, c.mergeId, &mergeRequestDiscussionOptions, nil) if err != nil { - return fmt.Errorf("Listing discussions failed: %w", err) + return nil, res.Response.StatusCode, fmt.Errorf("Listing discussions failed: %w", err) } var realDiscussions []*gitlab.Discussion @@ -52,9 +58,35 @@ func (c *Client) ListDiscussions() error { sortedDiscussions := SortableDiscussions(realDiscussions) sort.Sort(sortedDiscussions) - discussionsOutput, err := json.Marshal(sortedDiscussions) - - fmt.Println(string(discussionsOutput)) - - return nil + return sortedDiscussions, http.StatusOK, nil +} + +func ListDiscussionsHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + c := r.Context().Value("client").(Client) + msg, status, err := c.ListDiscussions() + + if err != nil { + response := ErrorResponse{ + Message: err.Error(), + Status: status, + } + json.NewEncoder(w).Encode(response) + return + } + + response := DiscussionsResponse{ + SuccessResponse: SuccessResponse{ + Message: "Discussions successfully fetched.", + Status: http.StatusOK, + }, + Discussions: msg, + } + + json.NewEncoder(w).Encode(response) } diff --git a/cmd/main.go b/cmd/main.go index 7a7d799..ae718bf 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,61 +1,59 @@ package main import ( + "context" "fmt" "log" + "net/http" "os" "os/exec" "strings" ) -const ( - star = "star" - info = "info" - approve = "approve" - revoke = "revoke" - comment = "comment" - overviewComment = "overviewComment" - deleteComment = "deleteComment" - editComment = "editComment" - reply = "reply" - listDiscussions = "listDiscussions" -) - func main() { branchName, err := getCurrentBranch() errCheck(err) if branchName == "main" || branchName == "master" { - return + log.Fatalf("Cannot run on %s branch", branchName) } + /* Initialize Gitlab client */ var c Client errCheck(c.Init(branchName)) - switch c.command { - case star: - errCheck(c.Star()) - case approve: - errCheck(c.Approve()) - case revoke: - errCheck(c.Revoke()) - case comment: - errCheck(c.Comment()) - case deleteComment: - errCheck(c.DeleteComment()) - case editComment: - errCheck(c.EditComment()) - case overviewComment: - errCheck(c.OverviewComment()) - case info: - errCheck(c.Info()) - case reply: - errCheck(c.Reply()) - case listDiscussions: - errCheck(c.ListDiscussions()) - default: - c.Usage("command") + m := http.NewServeMux() + m.Handle("/approve", withGitlabContext(http.HandlerFunc(ApproveHandler), c)) + m.Handle("/revoke", withGitlabContext(http.HandlerFunc(RevokeHandler), c)) + m.Handle("/star", withGitlabContext(http.HandlerFunc(StarHandler), c)) + m.Handle("/info", withGitlabContext(http.HandlerFunc(InfoHandler), c)) + m.Handle("/discussions", withGitlabContext(http.HandlerFunc(ListDiscussionsHandler), c)) + m.Handle("/comment", withGitlabContext(http.HandlerFunc(CommentHandler), c)) + m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c)) + + server := &http.Server{ + Addr: fmt.Sprintf(":%s", os.Args[2]), + Handler: m, } + + done := make(chan bool) + go server.ListenAndServe() + + /* This print is detected by the Lua code and used to fetch project information */ + fmt.Println("Server started.") + + <-done +} + +type ResponseError struct { + message string +} + +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) + next.ServeHTTP(w, r.WithContext(ctx)) + }) } func errCheck(err error) { diff --git a/cmd/reply.go b/cmd/reply.go index 875f3cd..9596757 100644 --- a/cmd/reply.go +++ b/cmd/reply.go @@ -2,40 +2,90 @@ package main import ( "encoding/json" - "errors" "fmt" - "os" + "io" + "net/http" "time" "github.com/xanzy/go-gitlab" ) -func (c *Client) Reply() error { +type ReplyRequest struct { + DiscussionId string `json:"discussion_id"` + Reply string `json:"reply"` +} - if len(os.Args) < 5 { - return errors.New("Must provide discussionId and reply text") - } +type ReplyResponse struct { + SuccessResponse + Note *gitlab.Note `json:"note"` +} - discussionId, reply := os.Args[3], os.Args[4] +func (c *Client) Reply(r ReplyRequest) (*gitlab.Note, int, error) { now := time.Now() options := gitlab.AddMergeRequestDiscussionNoteOptions{ - Body: gitlab.String(reply), + Body: gitlab.String(r.Reply), CreatedAt: &now, } - gitlabNote, _, err := c.git.Discussions.AddMergeRequestDiscussionNote(c.projectId, c.mergeId, discussionId, &options) + note, res, err := c.git.Discussions.AddMergeRequestDiscussionNote(c.projectId, c.mergeId, r.DiscussionId, &options) if err != nil { - return fmt.Errorf("Could not leave reply: %w", err) + return nil, res.Response.StatusCode, fmt.Errorf("Could not leave reply: %w", err) } - output, err := json.Marshal(gitlabNote) - if err != nil { - return fmt.Errorf("Could not marshal note: %w", err) - } - - fmt.Println(string(output)) - - return nil + return note, http.StatusOK, nil +} + +func ReplyHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + c := r.Context().Value("client").(Client) + + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + errMsg := map[string]string{"message": "Could not read request body"} + jsonMsg, _ := json.Marshal(errMsg) + w.Write(jsonMsg) + return + } + + defer r.Body.Close() + var replyRequest ReplyRequest + err = json.Unmarshal(body, &replyRequest) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + errMsg := map[string]string{"message": "Could not read JSON from request"} + jsonMsg, _ := json.Marshal(errMsg) + w.Write(jsonMsg) + return + } + + note, status, err := c.Reply(replyRequest) + w.Header().Set("Content-Type", "application/json") + + if err != nil { + response := ErrorResponse{ + Message: err.Error(), + Status: status, + } + json.NewEncoder(w).Encode(response) + return + } + + response := ReplyResponse{ + SuccessResponse: SuccessResponse{ + Message: fmt.Sprintf("Replied: %s", note.Body), + Status: http.StatusOK, + }, + Note: note, + } + + json.NewEncoder(w).Encode(response) } diff --git a/cmd/revoke.go b/cmd/revoke.go index 8a1d135..5de93b2 100644 --- a/cmd/revoke.go +++ b/cmd/revoke.go @@ -1,18 +1,47 @@ package main import ( + "encoding/json" "fmt" + "net/http" ) -func (c *Client) Revoke() error { +func (c *Client) Revoke() (string, int, error) { - _, err := c.git.MergeRequestApprovals.UnapproveMergeRequest(c.projectId, c.mergeId, nil, nil) + res, err := c.git.MergeRequestApprovals.UnapproveMergeRequest(c.projectId, c.mergeId, nil, nil) if err != nil { - return fmt.Errorf("Revoking approval failed: %w", err) + return "", res.Response.StatusCode, fmt.Errorf("Revoking approval failed: %w", err) } - fmt.Println("Success! Revoked MR approval.") + return "Success! Revoked MR approval.", http.StatusOK, nil - return nil +} + +func RevokeHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + client := r.Context().Value("client").(Client) + msg, status, err := client.Revoke() + w.WriteHeader(status) + + if err != nil { + response := ErrorResponse{ + Message: err.Error(), + Status: status, + } + json.NewEncoder(w).Encode(response) + return + } + + response := SuccessResponse{ + Message: msg, + Status: http.StatusOK, + } + + json.NewEncoder(w).Encode(response) } diff --git a/cmd/star.go b/cmd/star.go index c8d873c..460f618 100644 --- a/cmd/star.go +++ b/cmd/star.go @@ -1,17 +1,46 @@ package main import ( + "encoding/json" "fmt" - "log" + "net/http" + + "github.com/xanzy/go-gitlab" ) -func (c *Client) Star() error { - project, _, err := c.git.Projects.StarProject(c.projectId) +func (c *Client) Star() (*gitlab.Project, int, error) { + project, res, err := c.git.Projects.StarProject(c.projectId) if err != nil { - return fmt.Errorf("Starring project failed: %w", err) + return nil, res.Response.StatusCode, fmt.Errorf("Starring project failed: %w", err) } - log.Printf("Success! Starred project: %s", project.Name) + return project, http.StatusOK, nil - return nil +} + +func StarHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + w.Header().Set("Content-Type", "application/json") + client := r.Context().Value("client").(Client) + project, status, err := client.Star() + + w.Header().Set("Content-Type", "application/json") + if err != nil { + response := ErrorResponse{ + Message: err.Error(), + Status: status, + } + json.NewEncoder(w).Encode(response) + return + } + + response := SuccessResponse{ + Message: fmt.Sprintf("Starred project %s successfully!", project.Name), + Status: http.StatusOK, + } + + json.NewEncoder(w).Encode(response) } diff --git a/cmd/start.go b/cmd/start.go new file mode 100644 index 0000000..d589efc --- /dev/null +++ b/cmd/start.go @@ -0,0 +1,12 @@ +package main + +import ( + "fmt" + "os" +) + +func (c *Client) Start() error { + processId := os.Getpid() + fmt.Println(processId) + return nil +} diff --git a/cmd/types.go b/cmd/types.go new file mode 100644 index 0000000..895e5bf --- /dev/null +++ b/cmd/types.go @@ -0,0 +1,11 @@ +package main + +type ErrorResponse struct { + Message string `json:"message"` + Status int `json:"status"` +} + +type SuccessResponse struct { + Message string `json:"message"` + Status int `json:"status"` +} diff --git a/logs b/logs new file mode 100644 index 0000000..77e0c06 --- /dev/null +++ b/logs @@ -0,0 +1 @@ +[DEBUG] GET https://gitlab.com/api/v4/merge_requests?source_branch=develop&state=opened diff --git a/lua/gitlab/approve.lua b/lua/gitlab/approve.lua deleted file mode 100644 index 4107b75..0000000 --- a/lua/gitlab/approve.lua +++ /dev/null @@ -1,18 +0,0 @@ -local u = require("gitlab.utils") -local state = require("gitlab.state") -local Job = require("plenary.job") -local M = {} - --- Approves the current merge request -M.approve = function() - if u.base_invalid() then return end - Job:new({ - command = state.BIN, - args = { "approve", state.PROJECT_ID }, - on_stdout = u.print_success, - on_stderr = u.print_error - }):start() -end - - -return M diff --git a/lua/gitlab/comment.lua b/lua/gitlab/comment.lua index d594917..371f2a5 100644 --- a/lua/gitlab/comment.lua +++ b/lua/gitlab/comment.lua @@ -1,7 +1,7 @@ local Menu = require("nui.menu") local NuiTree = require("nui.tree") local notify = require("notify") -local Job = require("plenary.job") +local job = require("gitlab.job") local state = require("gitlab.state") local u = require("gitlab.utils") local keymaps = require("gitlab.keymaps") @@ -56,20 +56,9 @@ M.confirm_create_comment = function(text) end end - Job:new({ - command = state.BIN, - args = { - "comment", - state.PROJECT_ID, - current_line_number, - relative_file_path, - text, - sha - }, - -- TODO: Render the tree after comment creation. Refresh? - on_stdout = u.print_success, - on_stderr = u.print_error - }):start() + local json = string.format('{ "line_number": %d, "file_name": "%s", "comment": "%s" }', current_line_number, + relative_file_path, text) + job.run_job("comment", "POST", json) end M.delete_comment = function() @@ -118,29 +107,18 @@ M.delete_comment = function() local discussion_id = node:get_id() discussion_id = string.sub(discussion_id, 2) -- Remove the "-" at the start note_id = string.sub(note_id, 2) -- Remove the "-" at the start - Job:new({ - command = state.BIN, - args = { - "deleteComment", - state.PROJECT_ID, - discussion_id, - note_id, - }, - on_stdout = function(_, line) - vim.schedule(function() - if line ~= nil and line ~= "" then - notify(line, "info") - state.tree:remove_node("-" .. note_id) - local discussion_node = state.tree:get_node("-" .. discussion_id) - if not discussion_node:has_children() then - state.tree:remove_node("-" .. discussion_id) - end - state.tree:render() - end - end) - end, - on_stderr = u.print_error - }):start() + + + local json = string.format('{"discussion_id": "%s", "note_id": %d}', discussion_id, note_id) + job.run_job("comment", "DELETE", json, function(data) + notify(data.message, "success") + state.tree:remove_node("-" .. note_id) + local discussion_node = state.tree:get_node("-" .. discussion_id) + if not discussion_node:has_children() then + state.tree:remove_node("-" .. discussion_id) + end + state.tree:render() + end) end end, }) @@ -179,45 +157,31 @@ M.edit_comment = function() end M.send_edits = function(text) - Job:new({ - command = state.BIN, - args = { - "editComment", - state.PROJECT_ID, - state.ACTIVE_DISCUSSION, - state.ACTIVE_NOTE, - text, - }, - on_stdout = function(_, line) - local note = vim.json.decode(line) - if note == nil then - notify("There was an issue editing the note", "error") - return + local escapedText = string.gsub(text, "\n", "\\n") + local json = string.format('{"discussion_id": "%s", "note_id": %s, "comment": "%s"}', state.ACTIVE_DISCUSSION, + state.ACTIVE_NOTE, escapedText) + + job.run_job("comment", "PATCH", json, function() + vim.schedule(function() + local node = state.tree:get_node("-" .. state.ACTIVE_NOTE) + local childrenIds = node:get_child_ids() + for _, value in ipairs(childrenIds) do + state.tree:remove_node(value) end - vim.schedule(function() - local node = state.tree:get_node("-" .. state.ACTIVE_NOTE) + local newNoteTextNodes = {} + for bodyLine in text:gmatch("[^\n]+") do + table.insert(newNoteTextNodes, NuiTree.Node({ text = bodyLine, is_body = true }, {})) + end - local childrenIds = node:get_child_ids() - for _, value in ipairs(childrenIds) do - state.tree:remove_node(value) - end + state.tree:set_nodes(newNoteTextNodes, "-" .. state.ACTIVE_NOTE) - local newNoteTextNodes = {} - for bodyLine in note.body:gmatch("[^\n]+") do - table.insert(newNoteTextNodes, NuiTree.Node({ text = bodyLine, is_body = true }, {})) - end - - state.tree:set_nodes(newNoteTextNodes, "-" .. state.ACTIVE_NOTE) - - state.tree:render() - local buf = vim.api.nvim_get_current_buf() - u.darken_metadata(buf, '') - notify("Edited comment!") - end) - end, - on_stderr = u.print_error - }):start() + state.tree:render() + local buf = vim.api.nvim_get_current_buf() + u.darken_metadata(buf, '') + notify("Edited comment!") + end) + end) end return M diff --git a/lua/gitlab/discussions.lua b/lua/gitlab/discussions.lua index e054d00..4f697b6 100644 --- a/lua/gitlab/discussions.lua +++ b/lua/gitlab/discussions.lua @@ -1,5 +1,6 @@ local u = require("gitlab.utils") local NuiTree = require("nui.tree") +local job = require("gitlab.job") local notify = require("notify") local state = require("gitlab.state") local Job = require("plenary.job") @@ -17,87 +18,79 @@ M.reply = function() end M.send_reply = function(text) - Job:new({ - command = state.BIN, - args = { - "reply", - state.PROJECT_ID, - state.ACTIVE_DISCUSSION, - text, - }, - on_stdout = function(_, line) - local note = vim.json.decode(line) - if note == nil then - notify("There was an issue creating the note", "error") - return - end + local escapedText = string.gsub(text, "\n", "\\n") + local json = string.format('{"discussion_id": "%s", "reply": "%s"}', state.ACTIVE_DISCUSSION, escapedText) + job.run_job("reply", "POST", json, function(data) + local note_node = M.build_note(data.note) + note_node:expand() - local note_node = M.build_note(note) - note_node:expand() - - state.tree:add_node(note_node, "-" .. state.ACTIVE_DISCUSSION) - vim.schedule(function() - state.tree:render() - local buf = vim.api.nvim_get_current_buf() - u.darken_metadata(buf, '') - notify("Sent reply!") - end) - end, - on_stderr = u.print_error - }):start() + state.tree:add_node(note_node, "-" .. state.ACTIVE_DISCUSSION) + vim.schedule(function() + state.tree:render() + local buf = vim.api.nvim_get_current_buf() + u.darken_metadata(buf, '') + notify("Sent reply!") + end) + end) end -- Places all of the discussions into a readable list M.list_discussions = function() if u.base_invalid() then return end Job:new({ - command = state.BIN, - args = { "listDiscussions", state.PROJECT_ID }, - on_stdout = function(_, line) - local discussions = vim.json.decode(line) - if (type(discussions) == 'userdata') then - notify("No discussions found for this MR", "warn") - return - end - M.discussions = discussions - vim.schedule(function() - vim.cmd.tabnew() - local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_command("vsplit") - vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown') - vim.api.nvim_set_current_buf(buf) - local allDiscussions = {} - for i, discussion in ipairs(discussions) do - local discussionChildren = {} - for _, note in ipairs(discussion.notes) do - local note_node = M.build_note(note) - if i == 1 then - note_node:expand() - end - table.insert(discussionChildren, note_node) - end - local discussionNode = NuiTree.Node({ - text = discussion.id, - id = discussion.id, - is_discussion = true - }, - discussionChildren) - if i == 1 then - discussionNode:expand() - end - table.insert(allDiscussions, discussionNode) + command = "curl", + args = { "-s", "localhost:8081/" .. "discussions" }, + on_stdout = function(_, output) + local data_ok, data = pcall(vim.json.decode, output) + if data_ok and data ~= nil then + local status = (data.status >= 200 and data.status < 300) and "success" or "error" + if status == "error" then + notify("Could not fetch discussions!", "error") + return end - state.tree = NuiTree({ nodes = allDiscussions, bufnr = buf }) + M.discussions = data.discussions + vim.schedule(function() + vim.cmd.tabnew() + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_command("vsplit") + vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown') + vim.api.nvim_set_current_buf(buf) + local allDiscussions = {} + for i, discussion in ipairs(data.discussions) do + local discussionChildren = {} + for _, note in ipairs(discussion.notes) do + local note_node = M.build_note(note) + if i == 1 then + note_node:expand() + end + table.insert(discussionChildren, note_node) + end + local discussionNode = NuiTree.Node({ + text = discussion.id, + id = discussion.id, + is_discussion = true + }, + discussionChildren) + if i == 1 then + discussionNode:expand() + end + table.insert(allDiscussions, discussionNode) + end + state.tree = NuiTree({ nodes = allDiscussions, bufnr = buf }) - M.set_tree_keymaps(buf) + M.set_tree_keymaps(buf) - state.tree:render() - vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown') - u.darken_metadata(buf, '') - M.jump_to_file() - end) + state.tree:render() + vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown') + u.darken_metadata(buf, '') + M.jump_to_file() + end) + end + end, + on_stderr = function(_, output) + notify("Could not run approve command!", "error") + error(output) end, - on_stderr = u.print_error, }):start() end diff --git a/lua/gitlab/init.lua b/lua/gitlab/init.lua index a73c10d..cee060b 100644 --- a/lua/gitlab/init.lua +++ b/lua/gitlab/init.lua @@ -1,19 +1,18 @@ -local Job = require("plenary.job") +local curl = require("plenary.curl") local state = require("gitlab.state") local notify = require("notify") local discussions = require("gitlab.discussions") local summary = require("gitlab.summary") local keymaps = require("gitlab.keymaps") local comment = require("gitlab.comment") -local approve = require("gitlab.approve") -local revoke = require("gitlab.revoke") +local job = require("gitlab.job") local u = require("gitlab.utils") -- Root Module Scope local M = {} M.summary = summary.summary -M.approve = approve.approve -M.revoke = revoke.revoke +M.approve = job.approve +M.revoke = job.revoke M.create_comment = comment.create_comment M.list_discussions = discussions.list_discussions M.edit_comment = comment.edit_comment @@ -21,8 +20,6 @@ M.delete_comment = comment.delete_comment M.reply = discussions.reply -- Builds the Go binary, initializes the plugin, fetches MR info -local projectData = {} - M.build = function(args) if args == nil then args = {} end local command = string.format("cd %s && make", state.BIN_PATH) @@ -42,6 +39,9 @@ M.setup = function(args, build_only) state.BIN = parent_dir .. "/bin" if args == nil then args = {} end + if args.dev == true then + M.build(args) + end local binExists = io.open(state.BIN, "r") if not binExists or args.dev == true then @@ -78,29 +78,34 @@ M.setup = function(args, build_only) state.BASE_BRANCH = args.base_branch end + local error_message = "Failed to set up gitlab.nvim, could not get project information." if u.is_gitlab_repo() then - Job:new({ - command = state.BIN, - args = { "info", state.PROJECT_ID }, - on_stdout = function(_, line) - table.insert(projectData, line) - end, - on_stderr = u.print_error, - on_exit = function() - if projectData[1] ~= nil then - local parsed_ok, data = pcall(vim.json.decode, projectData[1]) - if parsed_ok ~= true then - notify("Failed calling setup. Could not get project data.", "error") - else - state.INFO = data + local port = args.port or 21036 + vim.fn.jobstart(state.BIN .. " " .. state.PROJECT_ID .. " " .. port, { + on_stdout = function(job_id) + if job_id <= 0 then + notify(error_message, "error") + return + else + local response_ok, response = pcall(curl.get, "localhost:" .. port .. "/info", + { timeout = 750 }) + if response == nil or not response_ok then + notify(error_message, "error") + return end + local body = response.body + local parsed_ok, data = pcall(vim.json.decode, body) + if parsed_ok ~= true then + notify(error_message, "error") + return + end + state.INFO = data + keymaps.set_keymap_keys(args.keymaps) + keymaps.set_keymaps() end - end, - }):start() + end + }) end - - keymaps.set_keymap_keys(args.keymaps) - keymaps.set_keymaps() end M.current_file_path = function() diff --git a/lua/gitlab/job.lua b/lua/gitlab/job.lua new file mode 100644 index 0000000..257358e --- /dev/null +++ b/lua/gitlab/job.lua @@ -0,0 +1,46 @@ +local notify = require("notify") +local Job = require("plenary.job") +local M = {} + +M.run_job = function(endpoint, method, body, callback) + local args = { "-s", "-X", (method or "POST"), "localhost:8081/" .. endpoint } + + if body ~= nil then + table.insert(args, 1, "-d") + table.insert(args, 2, body) + end + Job:new({ + command = "curl", + args = args, + on_stdout = function(_, output) + local data_ok, data = pcall(vim.json.decode, output) + if data_ok and data ~= nil then + local status = (data.status >= 200 and data.status < 300) and "success" or "error" + if callback ~= nil then + callback(data) + else + notify(data.message, status) + end + else + notify("Could not parse command output!", "error") + end + end, + on_stderr = function(_, output) + notify("Could not run command!", "error") + error(output) + end + }):start() +end + +-- Approves the current merge request +M.approve = function() + M.run_job("approve", "POST") +end + +-- Revokes approval for the current merge request +M.revoke = function() + M.run_job("revoke", "POST") +end + + +return M diff --git a/lua/gitlab/revoke.lua b/lua/gitlab/revoke.lua deleted file mode 100644 index 8dd48a3..0000000 --- a/lua/gitlab/revoke.lua +++ /dev/null @@ -1,17 +0,0 @@ -local u = require("gitlab.utils") -local state = require("gitlab.state") -local M = {} -local Job = require("plenary.job") - --- Revokes approval for the current merge request -M.revoke = function() - if u.base_invalid() then return end - Job:new({ - command = state.BIN, - args = { "revoke", state.PROJECT_ID }, - on_stdout = u.print_success, - on_stderr = u.print_error - }):start() -end - -return M diff --git a/lua/gitlab/utils/init.lua b/lua/gitlab/utils/init.lua index 2e2a71b..fd053b6 100644 --- a/lua/gitlab/utils/init.lua +++ b/lua/gitlab/utils/init.lua @@ -239,7 +239,6 @@ local split_diff_view_filename = function(filename) return hash, path end - M.get_relative_file_path = get_relative_file_path M.get_current_line_number = get_current_line_number M.get_buffer_text = get_buffer_text