BREAKING CHANGE: Setup refactor and code cleanup

This MR makes several major tweaks to the codebase. Primarily it adjusts
the setup steps for the application so that rather than providing just
the project ID in the `.gitlab.nvim` file, users can also provide a
vareity of other settings, such as auth_token, base_branch, and so
forth. This is to make the project more extensible in the future.

This MR also fixes a variety of issues with error handling in the code,
primarily in the request/response model between the Lua jobs and the
Golang server.

BREAKING CHANGE: Modifies `.gitlab.nvim` and setup steps
This commit is contained in:
Harrison (Harry) Cramer
2023-08-06 11:21:39 -04:00
committed by Harrison Cramer
parent ade9f81426
commit 4f0d4b49ef
16 changed files with 281 additions and 293 deletions

View File

@@ -19,13 +19,7 @@ https://user-images.githubusercontent.com/32515581/233739969-216dad6e-fa77-417f-
## Installation ## Installation
You'll need to have an environment variable available in your shell that you use to authenticate with Gitlab's API. It should look like this: With <a href="https://github.com/folke/lazy.nvim">Lazy</a>:
```bash
export GITLAB_TOKEN="your_gitlab_token"
```
Then install the plugin. Here's what it looks like with <a href="https://github.com/folke/lazy.nvim">Lazy</a>:
```lua ```lua
return { return {
@@ -57,32 +51,40 @@ use {
} }
``` ```
## Configuring per Gitlab Repository ## Configuration
By default, the plugin will not connect to a gitlab repository. You must add a `.gitlab.nvim` file to the root of your directory. The plugin will read that file and use it as the project ID. The file should only contain the ID of the project: This plugin requires a `.gitlab.nvim` file in the root of the local Gitlab directory. Provide this file with values required to connect to your gitlab instance (gitlab_url is optional, used for self-hosted instances):
``` ```
112415 project_id=112415
auth_token=your_gitlab_token
gitlab_url=https://my-personal-gitlab-instance.com
``` ```
The tool will look for and interact with MRs against a "main" branch. You can configure this by passing in the `base_branch` option: If you don't want to write your authentication token into a dotfile, you may provide it as a shell variable. For instance in your `.bashrc` or `.zshrc` file:
```lua ```bash
require('gitlab').setup({ base_branch = 'master' }) export AUTH_TOKEN="your_gitlab_token"
``` ```
If you are using `main` as your branch and you add a `.gitlab.nvim` configuration file, you can call an empty setup function and the plugin will work: By default, the plugin will interact with MRs against a "main" branch. You can configure this by passing in the `base_branch` option to the `.gitlab.nvim` configuration file for your project.
```lua
require('gitlab').setup()
``` ```
project_id=112415
auth_token=your_gitlab_token
gitlab_url=https://my-personal-gitlab-instance.com
base_branch=master
```
## Configuring the Plugin
Here is the default setup function: Here is the default setup function:
```lua ```lua
require("gitlab").setup({ require("gitlab").setup({
base_branch = "main",
port = 20136, -- The port of the Go server, which runs in the background port = 20136, -- The port of the Go server, which runs in the background
log_path = vim.fn.stdpath("cache"), -- Log path for the Go server
keymaps = { keymaps = {
popup = { -- The popup for comment creation, editing, and replying popup = { -- The popup for comment creation, editing, and replying
exit = "<Esc>", exit = "<Esc>",
@@ -189,3 +191,18 @@ Which looks like this in my editor:
<img width="1727" alt="Screenshot 2023-04-21 at 6 37 39 PM" src="https://user-images.githubusercontent.com/32515581/233744560-0d718c92-f810-4fde-b40d-8b6f42eb6f0e.png"> <img width="1727" alt="Screenshot 2023-04-21 at 6 37 39 PM" src="https://user-images.githubusercontent.com/32515581/233744560-0d718c92-f810-4fde-b40d-8b6f42eb6f0e.png">
This is useful if you plan to leave comments on the diff, because this plugin currently only supports leaving comments on lines that have been added or modified. I'm currenly working on adding functionality to allow users to leave comments on any lines, including those that have been deleted or untouched. This is useful if you plan to leave comments on the diff, because this plugin currently only supports leaving comments on lines that have been added or modified. I'm currenly working on adding functionality to allow users to leave comments on any lines, including those that have been deleted or untouched.
## Debugging
This plugin is built on top of a Golang server. If you want to debug that server, you can run it independently of Neovim. For instance, to start it up in a certain project, navigate to your plugin directory, and build the binary:
```bash
$ cd ~/.local/share/nvim/lazy/gitlab.nvim
$ cd cmd
$ go build -gcflags=all="-N -l" -o bin && cp ./bin ~/path-to-your-project
$ cd ~/path-to-your-project
$ dlv exec ./bin -- 41057709 https://www.gitlab.com 21036 your-gitlab-token
```
You can send JSON to it like you would any other REST server.

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
) )
@@ -18,25 +19,22 @@ func (c *Client) Approve() (string, int, error) {
} }
func ApproveHandler(w http.ResponseWriter, r *http.Request) { func ApproveHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
c := r.Context().Value("client").(Client)
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed) c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
return return
} }
msg, status, err := c.Approve()
w.Header().Set("Content-Type", "application/json")
client := r.Context().Value("client").(Client)
msg, status, err := client.Approve()
w.WriteHeader(status)
if err != nil { if err != nil {
response := ErrorResponse{ c.handleError(w, err, "Could not approve MR", http.StatusBadRequest)
Message: err.Error(),
Status: status,
}
json.NewEncoder(w).Encode(response)
return return
} }
/* TODO: Check for non-200 status codes */
w.WriteHeader(status)
response := SuccessResponse{ response := SuccessResponse{
Message: msg, Message: msg,
Status: http.StatusOK, Status: http.StatusOK,

View File

@@ -1,8 +1,10 @@
package main package main
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http"
"os" "os"
"strconv" "strconv"
@@ -13,6 +15,8 @@ type Client struct {
command string command string
projectId string projectId string
mergeId int mergeId int
gitlabInstance string
authToken string
git *gitlab.Client git *gitlab.Client
} }
@@ -22,7 +26,9 @@ type Logger struct {
func (l Logger) Printf(s string, args ...interface{}) { func (l Logger) Printf(s string, args ...interface{}) {
logString := fmt.Sprintf(s+"\n", args...) logString := fmt.Sprintf(s+"\n", args...)
file, err := os.OpenFile("./logs", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) logPath := os.Args[len(os.Args)-1]
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -31,21 +37,36 @@ func (l Logger) Printf(s string, args ...interface{}) {
} }
/* 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) < 2 { if len(os.Args) < 5 {
return errors.New("Must provide project ID!") return errors.New("Must provide project ID, gitlab instance, port, and auth token!")
} }
projectId := os.Args[1] projectId := os.Args[1]
c.projectId = projectId gitlabInstance := os.Args[2]
authToken := os.Args[4]
if projectId == "" { if projectId == "" {
return errors.New("Project ID cannot be empty") return errors.New("Project ID cannot be empty")
} }
if gitlabInstance == "" {
return errors.New("GitLab instance URL cannot be empty")
}
if authToken == "" {
return errors.New("Auth token cannot be empty")
}
c.gitlabInstance = gitlabInstance
c.projectId = projectId
c.authToken = authToken
var l Logger var l Logger
git, err := gitlab.NewClient(os.Getenv("GITLAB_TOKEN"), gitlab.WithCustomLogger(l)) var apiCustUrl = fmt.Sprintf(c.gitlabInstance + "/api/v4")
git, err := gitlab.NewClient(authToken, gitlab.WithBaseURL(apiCustUrl), 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)
@@ -77,3 +98,12 @@ func (c *Client) Init(branchName string) error {
return nil return nil
} }
func (c *Client) handleError(w http.ResponseWriter, err error, message string, status int) {
w.WriteHeader(status)
response := ErrorResponse{
Message: message,
Details: err.Error(),
}
json.NewEncoder(w).Encode(response)
}

View File

@@ -9,13 +9,12 @@ import (
"io/ioutil" "io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"time" "time"
"github.com/xanzy/go-gitlab" "github.com/xanzy/go-gitlab"
) )
const mrVersionsUrl = "https://gitlab.com/api/v4/projects/%s/merge_requests/%d/versions" const mrVersionsUrl = "%s/api/v4/projects/%s/merge_requests/%d/versions"
type MRVersion struct { type MRVersion struct {
ID int `json:"id"` ID int `json:"id"`
@@ -64,10 +63,7 @@ func DeleteComment(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
errMsg := map[string]string{"message": "Could not read request body"}
jsonMsg, _ := json.Marshal(errMsg)
w.Write(jsonMsg)
return return
} }
@@ -76,26 +72,20 @@ func DeleteComment(w http.ResponseWriter, r *http.Request) {
var deleteCommentRequest DeleteCommentRequest var deleteCommentRequest DeleteCommentRequest
err = json.Unmarshal(body, &deleteCommentRequest) err = json.Unmarshal(body, &deleteCommentRequest)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
errMsg := map[string]string{"message": "Could not read JSON from request"}
jsonMsg, _ := json.Marshal(errMsg)
w.Write(jsonMsg)
return return
} }
res, err := c.git.Discussions.DeleteMergeRequestDiscussionNote(c.projectId, c.mergeId, deleteCommentRequest.DiscussionId, deleteCommentRequest.NoteId) res, err := c.git.Discussions.DeleteMergeRequestDiscussionNote(c.projectId, c.mergeId, deleteCommentRequest.DiscussionId, deleteCommentRequest.NoteId)
w.WriteHeader(res.Response.StatusCode)
if err != nil { if err != nil {
response := ErrorResponse{ c.handleError(w, err, "Could not delete comment", res.StatusCode)
Message: err.Error(),
Status: res.Response.StatusCode,
}
json.NewEncoder(w).Encode(response)
return return
} }
w.WriteHeader(res.StatusCode)
/* TODO: Check status code */
response := SuccessResponse{ response := SuccessResponse{
Message: "Comment deleted succesfully", Message: "Comment deleted succesfully",
Status: http.StatusOK, Status: http.StatusOK,
@@ -110,10 +100,7 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
errMsg := map[string]string{"message": "Could not read request body"}
jsonMsg, _ := json.Marshal(errMsg)
w.Write(jsonMsg)
return return
} }
@@ -122,10 +109,7 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
var postCommentRequest PostCommentRequest var postCommentRequest PostCommentRequest
err = json.Unmarshal(body, &postCommentRequest) err = json.Unmarshal(body, &postCommentRequest)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
errMsg := map[string]string{"message": "Could not unmarshal data from request body"}
jsonMsg, _ := json.Marshal(errMsg)
w.Write(jsonMsg)
return return
} }
@@ -135,24 +119,19 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
} }
if err != nil { if err != nil {
response := ErrorResponse{ c.handleError(w, err, "Could not post comment", res.StatusCode)
Message: err.Error(),
Status: res.StatusCode,
}
json.NewEncoder(w).Encode(response)
return return
} }
w.WriteHeader(res.StatusCode)
/* TODO: Check for bad status codes */
response := SuccessResponse{ response := SuccessResponse{
Message: "Comment created succesfully", Message: "Comment created succesfully",
Status: http.StatusOK, Status: http.StatusOK,
} }
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
// w.WriteHeader(res.StatusCode)
// io.Copy(w, res.Body)
} }
func EditComment(w http.ResponseWriter, r *http.Request) { func EditComment(w http.ResponseWriter, r *http.Request) {
@@ -161,10 +140,7 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
errMsg := map[string]string{"message": "Could not read request body"}
jsonMsg, _ := json.Marshal(errMsg)
w.Write(jsonMsg)
return return
} }
@@ -173,10 +149,7 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
var editCommentRequest EditCommentRequest var editCommentRequest EditCommentRequest
err = json.Unmarshal(body, &editCommentRequest) err = json.Unmarshal(body, &editCommentRequest)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
errMsg := map[string]string{"message": "Could not unmarshal data from request body"}
jsonMsg, _ := json.Marshal(errMsg)
w.Write(jsonMsg)
return return
} }
@@ -191,11 +164,7 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
} }
if err != nil { if err != nil {
response := ErrorResponse{ c.handleError(w, err, "Could not edit comment", res.StatusCode)
Message: err.Error(),
Status: res.StatusCode,
}
json.NewEncoder(w).Encode(response)
return return
} }
@@ -209,7 +178,7 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
func (c *Client) PostComment(cr PostCommentRequest) (*http.Response, error) { func (c *Client) PostComment(cr PostCommentRequest) (*http.Response, error) {
err, response := getMRVersions(c.projectId, c.mergeId) err, response := getMRVersions(c.gitlabInstance, c.projectId, c.mergeId, c.authToken)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error making diff thread: %e", err) return nil, fmt.Errorf("Error making diff thread: %e", err)
} }
@@ -255,14 +224,12 @@ func min(a int, b int) int {
} }
/* Gets the latest merge request revision data */ /* Gets the latest merge request revision data */
func getMRVersions(projectId string, mergeId int) (e error, response *http.Response) { func getMRVersions(gitlabInstance string, projectId string, mergeId int, authToken string) (e error, response *http.Response) {
url := fmt.Sprintf(mrVersionsUrl, gitlabInstance, projectId, mergeId)
gitlabToken := os.Getenv("GITLAB_TOKEN")
url := fmt.Sprintf(mrVersionsUrl, projectId, mergeId)
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
req.Header.Add("PRIVATE-TOKEN", gitlabToken) req.Header.Add("PRIVATE-TOKEN", authToken)
if err != nil { if err != nil {
return err, nil return err, nil
@@ -288,7 +255,7 @@ 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) { 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(c.gitlabInstance+"/api/v4/projects/%s/merge_requests/%d/discussions", c.projectId, c.mergeId)
payload := &bytes.Buffer{} payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload) writer := multipart.NewWriter(payload)
@@ -322,7 +289,7 @@ func (c *Client) CommentOnDeletion(lineNumber int, fileName string, comment stri
if err != nil { if err != nil {
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", c.authToken)
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
res, err := client.Do(req) res, err := client.Do(req)

View File

@@ -1,26 +1,24 @@
package main package main
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
) )
const mrUrl = "https://gitlab.com/api/v4/projects/%s/merge_requests/%d" const mrUrl = "%s/api/v4/projects/%s/merge_requests/%d"
func (c *Client) Info() ([]byte, error) { func (c *Client) Info() ([]byte, error) {
url := fmt.Sprintf(mrUrl, c.projectId, c.mergeId) url := fmt.Sprintf(mrUrl, c.gitlabInstance, 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 nil, 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", c.authToken)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
@@ -46,19 +44,17 @@ func (c *Client) Info() ([]byte, error) {
} }
func InfoHandler(w http.ResponseWriter, r *http.Request) { func InfoHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
c := r.Context().Value("client").(Client)
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed) c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
return return
} }
w.Header().Set("Content-Type", "application/json") msg, err := c.Info()
client := r.Context().Value("client").(Client)
msg, err := client.Info()
if err != nil { if err != nil {
errResp := map[string]string{"message": err.Error()} c.handleError(w, err, "Could not get info", http.StatusBadRequest)
response, _ := json.Marshal(errResp)
w.WriteHeader(http.StatusInternalServerError)
w.Write(response)
return return
} }

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"sort" "sort"
@@ -62,24 +63,23 @@ func (c *Client) ListDiscussions() ([]*gitlab.Discussion, int, error) {
} }
func ListDiscussionsHandler(w http.ResponseWriter, r *http.Request) { func ListDiscussionsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
c := r.Context().Value("client").(Client)
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed) c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
return return
} }
w.Header().Set("Content-Type", "application/json")
c := r.Context().Value("client").(Client)
msg, status, err := c.ListDiscussions() msg, status, err := c.ListDiscussions()
if err != nil { if err != nil {
response := ErrorResponse{ c.handleError(w, err, "Could not list discussions", http.StatusBadRequest)
Message: err.Error(),
Status: status,
}
json.NewEncoder(w).Encode(response)
return return
} }
/* TODO: Check for non-200 statuses */
w.WriteHeader(status)
response := DiscussionsResponse{ response := DiscussionsResponse{
SuccessResponse: SuccessResponse{ SuccessResponse: SuccessResponse{
Message: "Discussions successfully fetched.", Message: "Discussions successfully fetched.",

View File

@@ -24,21 +24,21 @@ func main() {
/* Initialize Gitlab client */ /* Initialize Gitlab client */
var c Client var c Client
if err := c.Init(branchName); err != nil { if err := c.init(branchName); err != nil {
log.Fatalf("Failure: Failed to iniialize client: %v", err) log.Fatalf("Failure: Failed to initialize client: %v", err)
} }
m := http.NewServeMux() m := http.NewServeMux()
m.Handle("/approve", withGitlabContext(http.HandlerFunc(ApproveHandler), c)) m.Handle("/approve", withGitlabContext(http.HandlerFunc(ApproveHandler), c))
m.Handle("/revoke", withGitlabContext(http.HandlerFunc(RevokeHandler), 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("/info", withGitlabContext(http.HandlerFunc(InfoHandler), c))
m.Handle("/discussions", withGitlabContext(http.HandlerFunc(ListDiscussionsHandler), c)) m.Handle("/discussions", withGitlabContext(http.HandlerFunc(ListDiscussionsHandler), c))
m.Handle("/comment", withGitlabContext(http.HandlerFunc(CommentHandler), c)) m.Handle("/comment", withGitlabContext(http.HandlerFunc(CommentHandler), c))
m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c)) m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c))
port := fmt.Sprintf(":%s", os.Args[3])
server := &http.Server{ server := &http.Server{
Addr: fmt.Sprintf(":%s", os.Args[2]), Addr: port,
Handler: m, Handler: m,
} }

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -38,20 +39,17 @@ func (c *Client) Reply(r ReplyRequest) (*gitlab.Note, int, error) {
} }
func ReplyHandler(w http.ResponseWriter, r *http.Request) { func ReplyHandler(w http.ResponseWriter, r *http.Request) {
c := r.Context().Value("client").(Client)
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed) c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
return return
} }
w.Header().Set("Content-Type", "application/json")
c := r.Context().Value("client").(Client)
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
errMsg := map[string]string{"message": "Could not read request body"}
jsonMsg, _ := json.Marshal(errMsg)
w.Write(jsonMsg)
return return
} }
@@ -60,25 +58,18 @@ func ReplyHandler(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(body, &replyRequest) err = json.Unmarshal(body, &replyRequest)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
errMsg := map[string]string{"message": "Could not read JSON from request"}
jsonMsg, _ := json.Marshal(errMsg)
w.Write(jsonMsg)
return return
} }
note, status, err := c.Reply(replyRequest) note, status, err := c.Reply(replyRequest)
w.Header().Set("Content-Type", "application/json")
if err != nil { if err != nil {
response := ErrorResponse{ c.handleError(w, err, "Could not send reply", status)
Message: err.Error(),
Status: status,
}
json.NewEncoder(w).Encode(response)
return return
} }
w.WriteHeader(status)
response := ReplyResponse{ response := ReplyResponse{
SuccessResponse: SuccessResponse{ SuccessResponse: SuccessResponse{
Message: fmt.Sprintf("Replied: %s", note.Body), Message: fmt.Sprintf("Replied: %s", note.Body),

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
) )
@@ -19,25 +20,23 @@ func (c *Client) Revoke() (string, int, error) {
} }
func RevokeHandler(w http.ResponseWriter, r *http.Request) { func RevokeHandler(w http.ResponseWriter, r *http.Request) {
c := r.Context().Value("client").(Client)
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return return
} }
w.Header().Set("Content-Type", "application/json") msg, status, err := c.Revoke()
client := r.Context().Value("client").(Client)
msg, status, err := client.Revoke()
w.WriteHeader(status)
if err != nil { if err != nil {
response := ErrorResponse{ c.handleError(w, err, "Could not revoke approval", http.StatusBadRequest)
Message: err.Error(),
Status: status,
}
json.NewEncoder(w).Encode(response)
return return
} }
w.WriteHeader(status)
response := SuccessResponse{ response := SuccessResponse{
Message: msg, Message: msg,
Status: http.StatusOK, Status: http.StatusOK,

View File

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

View File

@@ -2,6 +2,7 @@ package main
type ErrorResponse struct { type ErrorResponse struct {
Message string `json:"message"` Message string `json:"message"`
Details string `json:"details"`
Status int `json:"status"` Status int `json:"status"`
} }

View File

@@ -55,8 +55,9 @@ M.confirm_create_comment = function(text)
end end
end end
local json = string.format('{ "line_number": %d, "file_name": "%s", "comment": "%s" }', current_line_number, local jsonTable = { line_number = current_line_number, file_name = relative_file_path, comment = text }
relative_file_path, text) local json = vim.json.encode(jsonTable)
job.run_job("comment", "POST", json) job.run_job("comment", "POST", json)
end end
@@ -105,10 +106,11 @@ M.delete_comment = function()
end end
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 = tonumber(string.sub(note_id, 2)) -- Remove the "-" at the start
local jsonTable = { discussion_id = discussion_id, note_id = note_id }
local json = vim.json.encode(jsonTable)
local json = string.format('{"discussion_id": "%s", "note_id": %d}', discussion_id, note_id)
job.run_job("comment", "DELETE", json, function(data) job.run_job("comment", "DELETE", json, function(data)
vim.notify(data.message, vim.log.levels.INFO) vim.notify(data.message, vim.log.levels.INFO)
state.tree:remove_node("-" .. note_id) state.tree:remove_node("-" .. note_id)
@@ -136,7 +138,7 @@ M.edit_comment = function()
editPopup:mount() editPopup:mount()
local note_id = string.sub(node:get_id(), 2) -- Remove the "-" at the start local note_id = tonumber(string.sub(node:get_id(), 2)) -- Remove the "-" at the start
local discussion_id = node:get_parent_id() local discussion_id = node:get_parent_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
@@ -157,8 +159,9 @@ end
M.send_edits = function(text) M.send_edits = function(text)
local escapedText = string.gsub(text, "\n", "\\n") 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) local jsonTable = { discussion_id = state.ACTIVE_DISCUSSION, note_id = state.ACTIVE_NOTE, comment = escapedText }
local json = vim.json.encode(jsonTable)
job.run_job("comment", "PATCH", json, function() job.run_job("comment", "PATCH", json, function()
vim.schedule(function() vim.schedule(function()

View File

@@ -18,7 +18,10 @@ end
M.send_reply = function(text) M.send_reply = function(text)
local escapedText = string.gsub(text, "\n", "\\n") local escapedText = string.gsub(text, "\n", "\\n")
local json = string.format('{"discussion_id": "%s", "reply": "%s"}', state.ACTIVE_DISCUSSION, escapedText)
local jsonTable = { discussion_id = state.ACTIVE_DISCUSSION, reply = escapedText }
local json = vim.json.encode(jsonTable)
job.run_job("reply", "POST", json, function(data) job.run_job("reply", "POST", json, function(data)
local note_node = M.build_note(data.note) local note_node = M.build_note(data.note)
note_node:expand() note_node:expand()

View File

@@ -18,8 +18,68 @@ M.edit_comment = comment.edit_comment
M.delete_comment = comment.delete_comment M.delete_comment = comment.delete_comment
M.reply = discussions.reply M.reply = discussions.reply
-- Builds the binary (if not built); starts the Go server; calls the /info endpoint,
-- which sets the Gitlab project's information in gitlab.nvim's INFO module
M.setup = function(args)
if args == nil then args = {} end
local file_path = u.current_file_path()
local parent_dir = vim.fn.fnamemodify(file_path, ":h:h:h:h")
state.BIN_PATH = parent_dir
state.BIN = parent_dir .. "/bin"
local binary_exists = vim.loop.fs_stat(state.BIN)
if binary_exists == nil then M.build() end
if not M.setPluginState(args) then return end -- Return if not a valid gitlab project
local command = state.BIN
.. " "
.. state.PROJECT_ID
.. " "
.. state.GITLAB_URL
.. " "
.. state.PORT
.. " "
.. state.AUTH_TOKEN
.. " "
.. state.LOG_PATH
vim.fn.jobstart(
command,
{
on_stdout = function(job_id)
if job_id <= 0 then
vim.notify("Could not start gitlab.nvim binary", vim.log.levels.ERROR)
return
else
local response_ok, response = pcall(
curl.get,
"localhost:" .. state.PORT .. "/info",
{ timeout = 750 }
)
if response == nil or not response_ok then
vim.notify("The gitlab.nvim server did not respond", vim.log.levels.ERROR)
print("Ran command: " .. command)
return
end
local body = response.body
local parsed_ok, data = pcall(vim.json.decode, body)
if parsed_ok ~= true then
vim.notify("The gitlab.nvim server returned an invalid response to the /info endpoint",
vim.log.levels.ERROR)
return
end
state.INFO = data
keymaps.set_keymap_keys(args.keymaps)
keymaps.set_keymaps()
end
end,
}
)
end
-- Builds the Go binary -- Builds the Go binary
local function build_binary() M.build = function()
local command = string.format("cd %s && make", state.BIN_PATH) local command = string.format("cd %s && make", state.BIN_PATH)
local installCode = os.execute(command .. "> /dev/null") local installCode = os.execute(command .. "> /dev/null")
if installCode ~= 0 then if installCode ~= 0 then
@@ -29,76 +89,61 @@ local function build_binary()
return true return true
end end
M.build = build_binary -- Initializes state for the project based on the arguments
-- provided in the `.gitlab.nvim` file per project, and the args provided in the setup function
-- Setups up the binary (if not built), starts the Go server, and calls the /info endpoint, M.setPluginState = function(args)
-- which sets the Gitlab project's information in gitlab.nvim's state module
M.setup = function(args)
local file_path = u.current_file_path()
local parent_dir = vim.fn.fnamemodify(file_path, ":h:h:h:h")
state.BIN_PATH = parent_dir
state.BIN = parent_dir .. "/bin"
if args == nil then args = {} end
local binary_exists = vim.loop.fs_stat(state.BIN)
if binary_exists == nil then
build_binary()
end
local config_file_path = vim.fn.getcwd() .. "/.gitlab.nvim" local config_file_path = vim.fn.getcwd() .. "/.gitlab.nvim"
local config_file_content = u.read_file(config_file_path) local config_file_content = u.read_file(config_file_path)
if config_file_content == nil then if config_file_content == nil then
return return false
end end
state.PROJECT_ID = config_file_content local file = assert(io.open(config_file_path, "r"))
if state.PROJECT_ID == nil then local property = {}
error("No project ID provided!") for line in file:lines() do
for key, value in string.gmatch(line, "(.-)=(.-)$") do
property[key] = value
end
end end
if type(tonumber(state.PROJECT_ID)) ~= 'number' then local project_id = property["project_id"]
error("The .gitlab.nvim project file may only contain a project number") local gitlab_url = property["gitlab_url"]
end local base_branch = property["base_branch"]
local auth_token = property["auth_token"]
if args.base_branch ~= nil then state.PROJECT_ID = project_id
state.BASE_BRANCH = args.base_branch state.AUTH_TOKEN = auth_token or os.getenv("GITLAB_TOKEN")
end state.GITLAB_URL = gitlab_url or "https://gitlab.com"
state.BASE_BRANCH = base_branch or "main"
local current_branch_raw = io.popen("git rev-parse --abbrev-ref HEAD"):read("*a") local current_branch_raw = io.popen("git rev-parse --abbrev-ref HEAD"):read("*a")
local current_branch = string.gsub(current_branch_raw, "\n", "") local current_branch = string.gsub(current_branch_raw, "\n", "")
if current_branch == state.BASE_BRANCH then if current_branch == state.BASE_BRANCH then
return return false
end end
if u.is_gitlab_repo() then if state.AUTH_TOKEN == nil then
error("Missing authentication token for Gitlab")
end
if state.AUTH_TOKEN == nil then
error("Missing authentication token for Gitlab")
end
if state.PROJECT_ID == nil then
error("Missing project ID in .gitlab.nvim file.")
end
if type(tonumber(state.PROJECT_ID)) ~= "number" then
error("The .gitlab.nvim project file's 'project_id' must be number")
end
-- Configuration for the plugin, such as port of server
state.PORT = args.port or 21036 state.PORT = args.port or 21036
vim.fn.jobstart(state.BIN .. " " .. state.PROJECT_ID .. " " .. state.PORT, { state.LOG_PATH = args.log_path or (vim.fn.stdpath("cache") .. "/gitlab.nvim.log")
on_stdout = function(job_id)
if job_id <= 0 then return true
vim.notify("Could not start gitlab.nvim binary", vim.log.levels.ERROR)
return
else
local response_ok, response = pcall(curl.get, "localhost:" .. state.PORT .. "/info",
{ timeout = 750 })
if response == nil or not response_ok then
vim.notify("The gitlab.nvim server did not respond", vim.log.levels.ERROR)
return
end
local body = response.body
local parsed_ok, data = pcall(vim.json.decode, body)
if parsed_ok ~= true then
vim.notify("The gitlab.nvim server returned an invalid response to the /info endpoint", vim.log.levels.ERROR)
return
end
state.INFO = data
keymaps.set_keymap_keys(args.keymaps)
keymaps.set_keymaps()
end
end
})
end
end end
return M return M

View File

@@ -9,25 +9,31 @@ M.run_job = function(endpoint, method, body, callback)
table.insert(args, 1, "-d") table.insert(args, 1, "-d")
table.insert(args, 2, body) table.insert(args, 2, body)
end end
-- This handler will handle all responses from the Go server. Anything with a successful
-- status will call the callback (if it is supplied for the job). Otherwise, it will print out the
-- success message or error message and details from the Go server.
Job:new({ Job:new({
command = "curl", command = "curl",
args = args, args = args,
on_stdout = function(_, output) on_stdout = function(_, output)
vim.defer_fn(function()
local data_ok, data = pcall(vim.json.decode, output) local data_ok, data = pcall(vim.json.decode, output)
if data_ok and data ~= nil then if data_ok and data ~= nil then
local status = (data.status >= 200 and data.status < 300) and "success" or "error" local status = (data.status >= 200 and data.status < 300) and "success" or "error"
if callback ~= nil then if status == "success" and callback ~= nil then
callback(data) callback(data)
elseif status == "success" then
local message = string.format("%s", data.message)
vim.notify(message, vim.log.levels.INFO)
else else
vim.defer_fn(function() local message = string.format("%s: %s", data.message, data.details)
vim.notify(data.message, vim.log.levels.DEBUG) vim.notify(message, vim.log.levels.DEBUG)
end, 0)
end end
else else
vim.defer_fn(function()
vim.notify("Could not parse command output!", vim.log.levels.ERROR) vim.notify("Could not parse command output!", vim.log.levels.ERROR)
end, 0)
end end
end, 0)
end, end,
on_stderr = function(_, output) on_stderr = function(_, output)
vim.defer_fn(function() vim.defer_fn(function()

View File

@@ -87,7 +87,9 @@ local base_invalid = function()
local base = state.BASE_BRANCH local base = state.BASE_BRANCH
local hasBaseBranch = feature_branch_exists(base) local hasBaseBranch = feature_branch_exists(base)
if not hasBaseBranch then if not hasBaseBranch then
vim.notify('No base branch. If this is a Gitlab repository, please check your setup function!', vim.log.levels.ERROR) vim.notify(
'Could not fetch feature branch. Please check that you have the correct base_branch value set in your .gitlab.nvim configuration file',
vim.log.levels.ERROR)
return true return true
end end
end end
@@ -107,29 +109,6 @@ local add_comment_sign = function(line_number)
vim.fn.sign_place(0, "piet", "piet", bufnr, { lnum = line_number }) vim.fn.sign_place(0, "piet", "piet", bufnr, { lnum = line_number })
end end
local function is_gitlab_repo()
local current_dir = vim.fn.getcwd()
-- check if it contains a .git folder
local git_dir = current_dir .. "/.git"
if vim.fn.isdirectory(git_dir) == 0 then
return false
end
local git_cmd = 'git remote get-url origin'
local handle = io.popen(git_cmd)
local result = handle:read("*a")
handle:close()
-- check if the remote URL is a Gitlab URL
if string.match(result, "gitlab%.com") then
return true
else
return false
end
end
local function jump_to_file(filename, line_number) local function jump_to_file(filename, line_number)
if line_number == nil then line_number = 1 end if line_number == nil then line_number = 1 end
vim.api.nvim_command("wincmd l") vim.api.nvim_command("wincmd l")
@@ -255,7 +234,6 @@ M.format_date = format_date
M.add_comment_sign = add_comment_sign M.add_comment_sign = add_comment_sign
M.jump_to_file = jump_to_file M.jump_to_file = jump_to_file
M.find_value_by_id = find_value_by_id M.find_value_by_id = find_value_by_id
M.is_gitlab_repo = is_gitlab_repo
M.darken_metadata = darken_metadata M.darken_metadata = darken_metadata
M.print_success = print_success M.print_success = print_success
M.print_error = print_error M.print_error = print_error