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:
committed by
Harrison Cramer
parent
ade9f81426
commit
4f0d4b49ef
51
README.md
51
README.md
@@ -19,13 +19,7 @@ https://user-images.githubusercontent.com/32515581/233739969-216dad6e-fa77-417f-
|
||||
|
||||
## 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:
|
||||
|
||||
```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>:
|
||||
With <a href="https://github.com/folke/lazy.nvim">Lazy</a>:
|
||||
|
||||
```lua
|
||||
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
|
||||
require('gitlab').setup({ base_branch = 'master' })
|
||||
```bash
|
||||
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:
|
||||
|
||||
```lua
|
||||
require("gitlab").setup({
|
||||
base_branch = "main",
|
||||
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 = {
|
||||
popup = { -- The popup for comment creation, editing, and replying
|
||||
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">
|
||||
|
||||
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.
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
@@ -18,25 +19,22 @@ func (c *Client) Approve() (string, int, error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
client := r.Context().Value("client").(Client)
|
||||
msg, status, err := client.Approve()
|
||||
w.WriteHeader(status)
|
||||
msg, status, err := c.Approve()
|
||||
|
||||
if err != nil {
|
||||
response := ErrorResponse{
|
||||
Message: err.Error(),
|
||||
Status: status,
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
c.handleError(w, err, "Could not approve MR", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
/* TODO: Check for non-200 status codes */
|
||||
w.WriteHeader(status)
|
||||
response := SuccessResponse{
|
||||
Message: msg,
|
||||
Status: http.StatusOK,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
@@ -10,10 +12,12 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
command string
|
||||
projectId string
|
||||
mergeId int
|
||||
git *gitlab.Client
|
||||
command string
|
||||
projectId string
|
||||
mergeId int
|
||||
gitlabInstance string
|
||||
authToken string
|
||||
git *gitlab.Client
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
@@ -22,7 +26,9 @@ type Logger struct {
|
||||
|
||||
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)
|
||||
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 {
|
||||
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 */
|
||||
func (c *Client) Init(branchName string) error {
|
||||
func (c *Client) init(branchName string) error {
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
return errors.New("Must provide project ID!")
|
||||
if len(os.Args) < 5 {
|
||||
return errors.New("Must provide project ID, gitlab instance, port, and auth token!")
|
||||
}
|
||||
|
||||
projectId := os.Args[1]
|
||||
c.projectId = projectId
|
||||
gitlabInstance := os.Args[2]
|
||||
authToken := os.Args[4]
|
||||
|
||||
if projectId == "" {
|
||||
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
|
||||
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 {
|
||||
return fmt.Errorf("Failed to create client: %v", err)
|
||||
@@ -77,3 +98,12 @@ func (c *Client) Init(branchName string) error {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -9,13 +9,12 @@ import (
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
ID int `json:"id"`
|
||||
@@ -64,10 +63,7 @@ func DeleteComment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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)
|
||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -76,26 +72,20 @@ func DeleteComment(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := c.git.Discussions.DeleteMergeRequestDiscussionNote(c.projectId, c.mergeId, deleteCommentRequest.DiscussionId, deleteCommentRequest.NoteId)
|
||||
|
||||
w.WriteHeader(res.Response.StatusCode)
|
||||
|
||||
if err != nil {
|
||||
response := ErrorResponse{
|
||||
Message: err.Error(),
|
||||
Status: res.Response.StatusCode,
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
c.handleError(w, err, "Could not delete comment", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(res.StatusCode)
|
||||
|
||||
/* TODO: Check status code */
|
||||
response := SuccessResponse{
|
||||
Message: "Comment deleted succesfully",
|
||||
Status: http.StatusOK,
|
||||
@@ -110,10 +100,7 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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)
|
||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,10 +109,7 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,24 +119,19 @@ func PostComment(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
response := ErrorResponse{
|
||||
Message: err.Error(),
|
||||
Status: res.StatusCode,
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
c.handleError(w, err, "Could not post comment", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(res.StatusCode)
|
||||
|
||||
/* TODO: Check for bad status codes */
|
||||
response := SuccessResponse{
|
||||
Message: "Comment created succesfully",
|
||||
Status: http.StatusOK,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
|
||||
// w.WriteHeader(res.StatusCode)
|
||||
// io.Copy(w, res.Body)
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
errMsg := map[string]string{"message": "Could not read request body"}
|
||||
jsonMsg, _ := json.Marshal(errMsg)
|
||||
w.Write(jsonMsg)
|
||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -173,10 +149,7 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -191,11 +164,7 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
response := ErrorResponse{
|
||||
Message: err.Error(),
|
||||
Status: res.StatusCode,
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
c.handleError(w, err, "Could not edit comment", res.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -209,7 +178,7 @@ func EditComment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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 {
|
||||
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 */
|
||||
func getMRVersions(projectId string, mergeId int) (e error, response *http.Response) {
|
||||
|
||||
gitlabToken := os.Getenv("GITLAB_TOKEN")
|
||||
url := fmt.Sprintf(mrVersionsUrl, projectId, mergeId)
|
||||
func getMRVersions(gitlabInstance string, projectId string, mergeId int, authToken string) (e error, response *http.Response) {
|
||||
url := fmt.Sprintf(mrVersionsUrl, gitlabInstance, projectId, mergeId)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
|
||||
req.Header.Add("PRIVATE-TOKEN", gitlabToken)
|
||||
req.Header.Add("PRIVATE-TOKEN", authToken)
|
||||
|
||||
if 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) {
|
||||
|
||||
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{}
|
||||
writer := multipart.NewWriter(payload)
|
||||
@@ -322,7 +289,7 @@ func (c *Client) CommentOnDeletion(lineNumber int, fileName string, comment stri
|
||||
if err != nil {
|
||||
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())
|
||||
|
||||
res, err := client.Do(req)
|
||||
|
||||
22
cmd/info.go
22
cmd/info.go
@@ -1,26 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"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) {
|
||||
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
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")
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
@@ -46,19 +44,17 @@ func (c *Client) Info() ([]byte, error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
client := r.Context().Value("client").(Client)
|
||||
msg, err := client.Info()
|
||||
msg, err := c.Info()
|
||||
if err != nil {
|
||||
errResp := map[string]string{"message": err.Error()}
|
||||
response, _ := json.Marshal(errResp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write(response)
|
||||
c.handleError(w, err, "Could not get info", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
@@ -62,24 +63,23 @@ func (c *Client) ListDiscussions() ([]*gitlab.Discussion, int, error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", 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)
|
||||
c.handleError(w, err, "Could not list discussions", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
/* TODO: Check for non-200 statuses */
|
||||
w.WriteHeader(status)
|
||||
response := DiscussionsResponse{
|
||||
SuccessResponse: SuccessResponse{
|
||||
Message: "Discussions successfully fetched.",
|
||||
|
||||
@@ -24,21 +24,21 @@ func main() {
|
||||
/* Initialize Gitlab client */
|
||||
var c Client
|
||||
|
||||
if err := c.Init(branchName); err != nil {
|
||||
log.Fatalf("Failure: Failed to iniialize client: %v", err)
|
||||
if err := c.init(branchName); err != nil {
|
||||
log.Fatalf("Failure: Failed to initialize client: %v", err)
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
port := fmt.Sprintf(":%s", os.Args[3])
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", os.Args[2]),
|
||||
Addr: port,
|
||||
Handler: m,
|
||||
}
|
||||
|
||||
|
||||
27
cmd/reply.go
27
cmd/reply.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -38,20 +39,17 @@ func (c *Client) Reply(r ReplyRequest) (*gitlab.Note, int, error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", 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)
|
||||
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,25 +58,18 @@ func ReplyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
c.handleError(w, err, "Could not read JSON from request", http.StatusBadRequest)
|
||||
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)
|
||||
c.handleError(w, err, "Could not send reply", status)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
response := ReplyResponse{
|
||||
SuccessResponse: SuccessResponse{
|
||||
Message: fmt.Sprintf("Replied: %s", note.Body),
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
@@ -19,25 +20,23 @@ func (c *Client) Revoke() (string, int, error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
|
||||
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)
|
||||
msg, status, err := c.Revoke()
|
||||
|
||||
if err != nil {
|
||||
response := ErrorResponse{
|
||||
Message: err.Error(),
|
||||
Status: status,
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
c.handleError(w, err, "Could not revoke approval", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
response := SuccessResponse{
|
||||
Message: msg,
|
||||
Status: http.StatusOK,
|
||||
|
||||
46
cmd/star.go
46
cmd/star.go
@@ -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)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,9 @@ M.confirm_create_comment = function(text)
|
||||
end
|
||||
end
|
||||
|
||||
local json = string.format('{ "line_number": %d, "file_name": "%s", "comment": "%s" }', current_line_number,
|
||||
relative_file_path, text)
|
||||
local jsonTable = { line_number = current_line_number, file_name = relative_file_path, comment = text }
|
||||
local json = vim.json.encode(jsonTable)
|
||||
|
||||
job.run_job("comment", "POST", json)
|
||||
end
|
||||
|
||||
@@ -105,10 +106,11 @@ M.delete_comment = function()
|
||||
end
|
||||
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
|
||||
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)
|
||||
vim.notify(data.message, vim.log.levels.INFO)
|
||||
state.tree:remove_node("-" .. note_id)
|
||||
@@ -136,9 +138,9 @@ M.edit_comment = function()
|
||||
|
||||
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()
|
||||
discussion_id = string.sub(discussion_id, 2) -- Remove the "-" at the start
|
||||
discussion_id = string.sub(discussion_id, 2) -- Remove the "-" at the start
|
||||
|
||||
state.ACTIVE_DISCUSSION = discussion_id
|
||||
state.ACTIVE_NOTE = note_id
|
||||
@@ -157,8 +159,9 @@ end
|
||||
|
||||
M.send_edits = function(text)
|
||||
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()
|
||||
vim.schedule(function()
|
||||
|
||||
@@ -18,7 +18,10 @@ end
|
||||
|
||||
M.send_reply = function(text)
|
||||
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)
|
||||
local note_node = M.build_note(data.note)
|
||||
note_node:expand()
|
||||
|
||||
@@ -18,8 +18,68 @@ M.edit_comment = comment.edit_comment
|
||||
M.delete_comment = comment.delete_comment
|
||||
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
|
||||
local function build_binary()
|
||||
M.build = function()
|
||||
local command = string.format("cd %s && make", state.BIN_PATH)
|
||||
local installCode = os.execute(command .. "> /dev/null")
|
||||
if installCode ~= 0 then
|
||||
@@ -29,76 +89,61 @@ local function build_binary()
|
||||
return true
|
||||
end
|
||||
|
||||
M.build = build_binary
|
||||
|
||||
-- Setups up the binary (if not built), starts the Go server, and calls the /info endpoint,
|
||||
-- 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
|
||||
|
||||
-- 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
|
||||
M.setPluginState = function(args)
|
||||
local config_file_path = vim.fn.getcwd() .. "/.gitlab.nvim"
|
||||
local config_file_content = u.read_file(config_file_path)
|
||||
if config_file_content == nil then
|
||||
return
|
||||
return false
|
||||
end
|
||||
|
||||
state.PROJECT_ID = config_file_content
|
||||
if state.PROJECT_ID == nil then
|
||||
error("No project ID provided!")
|
||||
local file = assert(io.open(config_file_path, "r"))
|
||||
local property = {}
|
||||
for line in file:lines() do
|
||||
for key, value in string.gmatch(line, "(.-)=(.-)$") do
|
||||
property[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
if type(tonumber(state.PROJECT_ID)) ~= 'number' then
|
||||
error("The .gitlab.nvim project file may only contain a project number")
|
||||
end
|
||||
local project_id = property["project_id"]
|
||||
local gitlab_url = property["gitlab_url"]
|
||||
local base_branch = property["base_branch"]
|
||||
local auth_token = property["auth_token"]
|
||||
|
||||
if args.base_branch ~= nil then
|
||||
state.BASE_BRANCH = args.base_branch
|
||||
end
|
||||
state.PROJECT_ID = project_id
|
||||
state.AUTH_TOKEN = auth_token or os.getenv("GITLAB_TOKEN")
|
||||
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 = string.gsub(current_branch_raw, "\n", "")
|
||||
|
||||
if current_branch == state.BASE_BRANCH then
|
||||
return
|
||||
return false
|
||||
end
|
||||
|
||||
if u.is_gitlab_repo() then
|
||||
state.PORT = args.port or 21036
|
||||
vim.fn.jobstart(state.BIN .. " " .. state.PROJECT_ID .. " " .. state.PORT, {
|
||||
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)
|
||||
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
|
||||
})
|
||||
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.LOG_PATH = args.log_path or (vim.fn.stdpath("cache") .. "/gitlab.nvim.log")
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -9,25 +9,31 @@ M.run_job = function(endpoint, method, body, callback)
|
||||
table.insert(args, 1, "-d")
|
||||
table.insert(args, 2, body)
|
||||
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({
|
||||
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)
|
||||
vim.defer_fn(function()
|
||||
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 == "success" and callback ~= nil then
|
||||
callback(data)
|
||||
elseif status == "success" then
|
||||
local message = string.format("%s", data.message)
|
||||
vim.notify(message, vim.log.levels.INFO)
|
||||
else
|
||||
local message = string.format("%s: %s", data.message, data.details)
|
||||
vim.notify(message, vim.log.levels.DEBUG)
|
||||
end
|
||||
else
|
||||
vim.defer_fn(function()
|
||||
vim.notify(data.message, vim.log.levels.DEBUG)
|
||||
end, 0)
|
||||
end
|
||||
else
|
||||
vim.defer_fn(function()
|
||||
vim.notify("Could not parse command output!", vim.log.levels.ERROR)
|
||||
end, 0)
|
||||
end
|
||||
end
|
||||
end, 0)
|
||||
end,
|
||||
on_stderr = function(_, output)
|
||||
vim.defer_fn(function()
|
||||
|
||||
@@ -87,7 +87,9 @@ local base_invalid = function()
|
||||
local base = state.BASE_BRANCH
|
||||
local hasBaseBranch = feature_branch_exists(base)
|
||||
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
|
||||
end
|
||||
end
|
||||
@@ -107,29 +109,6 @@ local add_comment_sign = function(line_number)
|
||||
vim.fn.sign_place(0, "piet", "piet", bufnr, { lnum = line_number })
|
||||
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)
|
||||
if line_number == nil then line_number = 1 end
|
||||
vim.api.nvim_command("wincmd l")
|
||||
@@ -255,7 +234,6 @@ M.format_date = format_date
|
||||
M.add_comment_sign = add_comment_sign
|
||||
M.jump_to_file = jump_to_file
|
||||
M.find_value_by_id = find_value_by_id
|
||||
M.is_gitlab_repo = is_gitlab_repo
|
||||
M.darken_metadata = darken_metadata
|
||||
M.print_success = print_success
|
||||
M.print_error = print_error
|
||||
|
||||
Reference in New Issue
Block a user