Change to HTTP Model (#5)

This commit is contained in:
Harrison (Harry) Cramer
2023-05-19 17:28:58 -07:00
committed by GitHub
parent fe0d09d582
commit 63fc025070
21 changed files with 689 additions and 430 deletions

View File

@@ -1,18 +1,46 @@
package main package main
import ( import (
"encoding/json"
"fmt" "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 { 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 "Success! Approved MR.", http.StatusOK, nil
}
return 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)
} }

View File

@@ -16,22 +16,37 @@ type Client struct {
git *gitlab.Client 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 */ /* 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 { func (c *Client) Init(branchName string) error {
if len(os.Args) < 3 { if len(os.Args) < 2 {
return errors.New("Must provide command and projectId") return errors.New("Must provide project ID!")
} }
command, projectId := os.Args[1], os.Args[2] projectId := os.Args[1]
c.command = command
c.projectId = projectId c.projectId = projectId
if 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 { if err != nil {
return fmt.Errorf("Failed to create client: %v", err) return fmt.Errorf("Failed to create client: %v", err)
} }
@@ -61,8 +76,3 @@ func (c *Client) Init(branchName string) error {
return nil return nil
} }
func (c *Client) Usage(command string) {
fmt.Printf("Usage: gitlab-nvim %s <project-id> ...args", command)
os.Exit(1)
}

View File

@@ -5,12 +5,11 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"log"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
"strconv"
"time" "time"
"github.com/xanzy/go-gitlab" "github.com/xanzy/go-gitlab"
@@ -29,20 +28,192 @@ type MRVersion struct {
RealSize string `json:"real_size"` RealSize string `json:"real_size"`
} }
func (c *Client) Comment() error { type PostCommentRequest struct {
if len(os.Args) < 6 { LineNumber int `json:"line_number"`
c.Usage("comment") 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] defer r.Body.Close()
if lineNumber == "" || fileName == "" || comment == "" {
c.Usage("comment") 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) err, response := getMRVersions(c.projectId, c.mergeId)
if err != nil { 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() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
@@ -50,7 +221,7 @@ func (c *Client) Comment() error {
var diffVersionInfo []MRVersion var diffVersionInfo []MRVersion
err = json.Unmarshal(body, &diffVersionInfo) err = json.Unmarshal(body, &diffVersionInfo)
if err != nil { 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 /* 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 */ 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++ { for i := 0; i < 3; i++ {
ii := i ii := i
_, err := c.CommentOnDeletion(lineNumber, fileName, comment, diffVersionInfo[0], ii) res, err := c.CommentOnDeletion(cr.LineNumber, cr.FileName, cr.Comment, diffVersionInfo[0], ii)
if err == nil {
fmt.Println("Left Comment: " + comment[0:min(len(comment), 25)] + "...") if err == nil && res.StatusCode >= 200 && res.StatusCode <= 299 {
return nil 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 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 😢 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) 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[old_path]", fileName)
_ = writer.WriteField("position[new_path]", fileName) _ = writer.WriteField("position[new_path]", fileName)
if i == 0 { if i == 0 {
_ = writer.WriteField("position[old_line]", lineNumber) _ = writer.WriteField("position[old_line]", fmt.Sprintf("%d", lineNumber))
} else if i == 1 { } else if i == 1 {
_ = writer.WriteField("position[new_line]", lineNumber) _ = writer.WriteField("position[new_line]", fmt.Sprintf("%d", lineNumber))
} else { } else {
_ = writer.WriteField("position[old_line]", lineNumber) _ = writer.WriteField("position[old_line]", fmt.Sprintf("%d", lineNumber))
_ = writer.WriteField("position[new_line]", lineNumber) _ = writer.WriteField("position[new_line]", fmt.Sprintf("%d", lineNumber))
} }
err := writer.Close() 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) return nil, fmt.Errorf("Error building request: %w", err)
} }
req.Header.Add("PRIVATE-TOKEN", os.Getenv("GITLAB_TOKEN")) req.Header.Add("PRIVATE-TOKEN", os.Getenv("GITLAB_TOKEN"))
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
res, err := client.Do(req) res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("Error making request: %w", err)
}
defer res.Body.Close() return res, err
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
} }

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -10,13 +11,13 @@ import (
const mrUrl = "https://gitlab.com/api/v4/projects/%s/merge_requests/%d" 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) url := fmt.Sprintf(mrUrl, c.projectId, c.mergeId)
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != 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")) req.Header.Set("PRIVATE-TOKEN", os.Getenv("GITLAB_TOKEN"))
@@ -25,22 +26,42 @@ func (c *Client) Info() error {
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { 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() defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 { 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) body, err := io.ReadAll(res.Body)
if err != nil { 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 */ /* 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)
} }

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"net/http"
"sort" "sort"
"encoding/json" "encoding/json"
@@ -11,6 +12,11 @@ import (
type SortableDiscussions []*gitlab.Discussion type SortableDiscussions []*gitlab.Discussion
type DiscussionsResponse struct {
SuccessResponse
Discussions []*gitlab.Discussion `json:"discussions"`
}
func (n SortableDiscussions) Len() int { func (n SortableDiscussions) Len() int {
return len(n) return len(n)
} }
@@ -26,16 +32,16 @@ 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() error { func (c *Client) ListDiscussions() ([]*gitlab.Discussion, int, error) {
mergeRequestDiscussionOptions := gitlab.ListMergeRequestDiscussionsOptions{ mergeRequestDiscussionOptions := gitlab.ListMergeRequestDiscussionsOptions{
Page: 1, Page: 1,
PerPage: 250, 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 { 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 var realDiscussions []*gitlab.Discussion
@@ -52,9 +58,35 @@ func (c *Client) ListDiscussions() error {
sortedDiscussions := SortableDiscussions(realDiscussions) sortedDiscussions := SortableDiscussions(realDiscussions)
sort.Sort(sortedDiscussions) sort.Sort(sortedDiscussions)
discussionsOutput, err := json.Marshal(sortedDiscussions) return sortedDiscussions, http.StatusOK, nil
}
fmt.Println(string(discussionsOutput))
func ListDiscussionsHandler(w http.ResponseWriter, r *http.Request) {
return nil 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)
} }

View File

@@ -1,61 +1,59 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"net/http"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
) )
const (
star = "star"
info = "info"
approve = "approve"
revoke = "revoke"
comment = "comment"
overviewComment = "overviewComment"
deleteComment = "deleteComment"
editComment = "editComment"
reply = "reply"
listDiscussions = "listDiscussions"
)
func main() { func main() {
branchName, err := getCurrentBranch() branchName, err := getCurrentBranch()
errCheck(err) errCheck(err)
if branchName == "main" || branchName == "master" { if branchName == "main" || branchName == "master" {
return log.Fatalf("Cannot run on %s branch", branchName)
} }
/* Initialize Gitlab client */
var c Client var c Client
errCheck(c.Init(branchName)) errCheck(c.Init(branchName))
switch c.command { m := http.NewServeMux()
case star: m.Handle("/approve", withGitlabContext(http.HandlerFunc(ApproveHandler), c))
errCheck(c.Star()) m.Handle("/revoke", withGitlabContext(http.HandlerFunc(RevokeHandler), c))
case approve: m.Handle("/star", withGitlabContext(http.HandlerFunc(StarHandler), c))
errCheck(c.Approve()) m.Handle("/info", withGitlabContext(http.HandlerFunc(InfoHandler), c))
case revoke: m.Handle("/discussions", withGitlabContext(http.HandlerFunc(ListDiscussionsHandler), c))
errCheck(c.Revoke()) m.Handle("/comment", withGitlabContext(http.HandlerFunc(CommentHandler), c))
case comment: m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c))
errCheck(c.Comment())
case deleteComment: server := &http.Server{
errCheck(c.DeleteComment()) Addr: fmt.Sprintf(":%s", os.Args[2]),
case editComment: Handler: m,
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")
} }
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) { func errCheck(err error) {

View File

@@ -2,40 +2,90 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "io"
"net/http"
"time" "time"
"github.com/xanzy/go-gitlab" "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 { type ReplyResponse struct {
return errors.New("Must provide discussionId and reply text") 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() now := time.Now()
options := gitlab.AddMergeRequestDiscussionNoteOptions{ options := gitlab.AddMergeRequestDiscussionNoteOptions{
Body: gitlab.String(reply), Body: gitlab.String(r.Reply),
CreatedAt: &now, 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 { 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) return note, http.StatusOK, nil
if err != nil { }
return fmt.Errorf("Could not marshal note: %w", err)
} func ReplyHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fmt.Println(string(output)) w.WriteHeader(http.StatusMethodNotAllowed)
return
return nil }
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)
} }

View File

@@ -1,18 +1,47 @@
package main package main
import ( import (
"encoding/json"
"fmt" "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 { 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)
} }

View File

@@ -1,17 +1,46 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "net/http"
"github.com/xanzy/go-gitlab"
) )
func (c *Client) Star() error { func (c *Client) Star() (*gitlab.Project, int, error) {
project, _, err := c.git.Projects.StarProject(c.projectId) project, res, err := c.git.Projects.StarProject(c.projectId)
if err != nil { 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)
} }

12
cmd/start.go Normal file
View File

@@ -0,0 +1,12 @@
package main
import (
"fmt"
"os"
)
func (c *Client) Start() error {
processId := os.Getpid()
fmt.Println(processId)
return nil
}

11
cmd/types.go Normal file
View File

@@ -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"`
}

1
logs Normal file
View File

@@ -0,0 +1 @@
[DEBUG] GET https://gitlab.com/api/v4/merge_requests?source_branch=develop&state=opened

View File

@@ -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

View File

@@ -1,7 +1,7 @@
local Menu = require("nui.menu") local Menu = require("nui.menu")
local NuiTree = require("nui.tree") local NuiTree = require("nui.tree")
local notify = require("notify") local notify = require("notify")
local Job = require("plenary.job") local job = require("gitlab.job")
local state = require("gitlab.state") local state = require("gitlab.state")
local u = require("gitlab.utils") local u = require("gitlab.utils")
local keymaps = require("gitlab.keymaps") local keymaps = require("gitlab.keymaps")
@@ -56,20 +56,9 @@ M.confirm_create_comment = function(text)
end end
end end
Job:new({ local json = string.format('{ "line_number": %d, "file_name": "%s", "comment": "%s" }', current_line_number,
command = state.BIN, relative_file_path, text)
args = { job.run_job("comment", "POST", json)
"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()
end end
M.delete_comment = function() M.delete_comment = function()
@@ -118,29 +107,18 @@ M.delete_comment = function()
local discussion_id = node:get_id() local discussion_id = node:get_id()
discussion_id = string.sub(discussion_id, 2) -- Remove the "-" at the start discussion_id = string.sub(discussion_id, 2) -- Remove the "-" at the start
note_id = string.sub(note_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 = { local json = string.format('{"discussion_id": "%s", "note_id": %d}', discussion_id, note_id)
"deleteComment", job.run_job("comment", "DELETE", json, function(data)
state.PROJECT_ID, notify(data.message, "success")
discussion_id, state.tree:remove_node("-" .. note_id)
note_id, local discussion_node = state.tree:get_node("-" .. discussion_id)
}, if not discussion_node:has_children() then
on_stdout = function(_, line) state.tree:remove_node("-" .. discussion_id)
vim.schedule(function() end
if line ~= nil and line ~= "" then state.tree:render()
notify(line, "info") end)
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()
end end
end, end,
}) })
@@ -179,45 +157,31 @@ M.edit_comment = function()
end end
M.send_edits = function(text) M.send_edits = function(text)
Job:new({ local escapedText = string.gsub(text, "\n", "\\n")
command = state.BIN, local json = string.format('{"discussion_id": "%s", "note_id": %s, "comment": "%s"}', state.ACTIVE_DISCUSSION,
args = { state.ACTIVE_NOTE, escapedText)
"editComment",
state.PROJECT_ID, job.run_job("comment", "PATCH", json, function()
state.ACTIVE_DISCUSSION, vim.schedule(function()
state.ACTIVE_NOTE, local node = state.tree:get_node("-" .. state.ACTIVE_NOTE)
text, local childrenIds = node:get_child_ids()
}, for _, value in ipairs(childrenIds) do
on_stdout = function(_, line) state.tree:remove_node(value)
local note = vim.json.decode(line)
if note == nil then
notify("There was an issue editing the note", "error")
return
end end
vim.schedule(function() local newNoteTextNodes = {}
local node = state.tree:get_node("-" .. state.ACTIVE_NOTE) for bodyLine in text:gmatch("[^\n]+") do
table.insert(newNoteTextNodes, NuiTree.Node({ text = bodyLine, is_body = true }, {}))
end
local childrenIds = node:get_child_ids() state.tree:set_nodes(newNoteTextNodes, "-" .. state.ACTIVE_NOTE)
for _, value in ipairs(childrenIds) do
state.tree:remove_node(value)
end
local newNoteTextNodes = {} state.tree:render()
for bodyLine in note.body:gmatch("[^\n]+") do local buf = vim.api.nvim_get_current_buf()
table.insert(newNoteTextNodes, NuiTree.Node({ text = bodyLine, is_body = true }, {})) u.darken_metadata(buf, '')
end notify("Edited comment!")
end)
state.tree:set_nodes(newNoteTextNodes, "-" .. state.ACTIVE_NOTE) end)
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()
end end
return M return M

View File

@@ -1,5 +1,6 @@
local u = require("gitlab.utils") local u = require("gitlab.utils")
local NuiTree = require("nui.tree") local NuiTree = require("nui.tree")
local job = require("gitlab.job")
local notify = require("notify") local notify = require("notify")
local state = require("gitlab.state") local state = require("gitlab.state")
local Job = require("plenary.job") local Job = require("plenary.job")
@@ -17,87 +18,79 @@ M.reply = function()
end end
M.send_reply = function(text) M.send_reply = function(text)
Job:new({ local escapedText = string.gsub(text, "\n", "\\n")
command = state.BIN, local json = string.format('{"discussion_id": "%s", "reply": "%s"}', state.ACTIVE_DISCUSSION, escapedText)
args = { job.run_job("reply", "POST", json, function(data)
"reply", local note_node = M.build_note(data.note)
state.PROJECT_ID, note_node:expand()
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 note_node = M.build_note(note) state.tree:add_node(note_node, "-" .. state.ACTIVE_DISCUSSION)
note_node:expand() vim.schedule(function()
state.tree:render()
state.tree:add_node(note_node, "-" .. state.ACTIVE_DISCUSSION) local buf = vim.api.nvim_get_current_buf()
vim.schedule(function() u.darken_metadata(buf, '')
state.tree:render() notify("Sent reply!")
local buf = vim.api.nvim_get_current_buf() end)
u.darken_metadata(buf, '') end)
notify("Sent reply!")
end)
end,
on_stderr = u.print_error
}):start()
end end
-- Places all of the discussions into a readable list -- Places all of the discussions into a readable list
M.list_discussions = function() M.list_discussions = function()
if u.base_invalid() then return end if u.base_invalid() then return end
Job:new({ Job:new({
command = state.BIN, command = "curl",
args = { "listDiscussions", state.PROJECT_ID }, args = { "-s", "localhost:8081/" .. "discussions" },
on_stdout = function(_, line) on_stdout = function(_, output)
local discussions = vim.json.decode(line) local data_ok, data = pcall(vim.json.decode, output)
if (type(discussions) == 'userdata') then if data_ok and data ~= nil then
notify("No discussions found for this MR", "warn") local status = (data.status >= 200 and data.status < 300) and "success" or "error"
return if status == "error" then
end notify("Could not fetch discussions!", "error")
M.discussions = discussions return
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)
end 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() state.tree:render()
vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown') vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown')
u.darken_metadata(buf, '') u.darken_metadata(buf, '')
M.jump_to_file() M.jump_to_file()
end) end)
end
end,
on_stderr = function(_, output)
notify("Could not run approve command!", "error")
error(output)
end, end,
on_stderr = u.print_error,
}):start() }):start()
end end

View File

@@ -1,19 +1,18 @@
local Job = require("plenary.job") local curl = require("plenary.curl")
local state = require("gitlab.state") local state = require("gitlab.state")
local notify = require("notify") local notify = require("notify")
local discussions = require("gitlab.discussions") local discussions = require("gitlab.discussions")
local summary = require("gitlab.summary") local summary = require("gitlab.summary")
local keymaps = require("gitlab.keymaps") local keymaps = require("gitlab.keymaps")
local comment = require("gitlab.comment") local comment = require("gitlab.comment")
local approve = require("gitlab.approve") local job = require("gitlab.job")
local revoke = require("gitlab.revoke")
local u = require("gitlab.utils") local u = require("gitlab.utils")
-- Root Module Scope -- Root Module Scope
local M = {} local M = {}
M.summary = summary.summary M.summary = summary.summary
M.approve = approve.approve M.approve = job.approve
M.revoke = revoke.revoke M.revoke = job.revoke
M.create_comment = comment.create_comment M.create_comment = comment.create_comment
M.list_discussions = discussions.list_discussions M.list_discussions = discussions.list_discussions
M.edit_comment = comment.edit_comment M.edit_comment = comment.edit_comment
@@ -21,8 +20,6 @@ M.delete_comment = comment.delete_comment
M.reply = discussions.reply M.reply = discussions.reply
-- Builds the Go binary, initializes the plugin, fetches MR info -- Builds the Go binary, initializes the plugin, fetches MR info
local projectData = {}
M.build = function(args) M.build = function(args)
if args == nil then args = {} end if args == nil then args = {} end
local command = string.format("cd %s && make", state.BIN_PATH) 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" state.BIN = parent_dir .. "/bin"
if args == nil then args = {} end if args == nil then args = {} end
if args.dev == true then
M.build(args)
end
local binExists = io.open(state.BIN, "r") local binExists = io.open(state.BIN, "r")
if not binExists or args.dev == true then if not binExists or args.dev == true then
@@ -78,29 +78,34 @@ M.setup = function(args, build_only)
state.BASE_BRANCH = args.base_branch state.BASE_BRANCH = args.base_branch
end end
local error_message = "Failed to set up gitlab.nvim, could not get project information."
if u.is_gitlab_repo() then if u.is_gitlab_repo() then
Job:new({ local port = args.port or 21036
command = state.BIN, vim.fn.jobstart(state.BIN .. " " .. state.PROJECT_ID .. " " .. port, {
args = { "info", state.PROJECT_ID }, on_stdout = function(job_id)
on_stdout = function(_, line) if job_id <= 0 then
table.insert(projectData, line) notify(error_message, "error")
end, return
on_stderr = u.print_error, else
on_exit = function() local response_ok, response = pcall(curl.get, "localhost:" .. port .. "/info",
if projectData[1] ~= nil then { timeout = 750 })
local parsed_ok, data = pcall(vim.json.decode, projectData[1]) if response == nil or not response_ok then
if parsed_ok ~= true then notify(error_message, "error")
notify("Failed calling setup. Could not get project data.", "error") return
else
state.INFO = data
end 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
end, end
}):start() })
end end
keymaps.set_keymap_keys(args.keymaps)
keymaps.set_keymaps()
end end
M.current_file_path = function() M.current_file_path = function()

46
lua/gitlab/job.lua Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -239,7 +239,6 @@ local split_diff_view_filename = function(filename)
return hash, path return hash, path
end end
M.get_relative_file_path = get_relative_file_path M.get_relative_file_path = get_relative_file_path
M.get_current_line_number = get_current_line_number M.get_current_line_number = get_current_line_number
M.get_buffer_text = get_buffer_text M.get_buffer_text = get_buffer_text