Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Remove Requirement for Dotfile #84

Merged
merged 12 commits into from
Nov 12, 2023
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,20 @@ use {

## Project Configuration

This plugin requires a `.gitlab.nvim` file in the root of the project. Provide this file with values required to connect to your gitlab instance of your repository (gitlab_url is optional, use ONLY for self-hosted instances):
This plugin requires an <a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token">auth token</a> to connect to Gitlab. The token can be set in the root directory of the project in a `.gitlab.nvim` environment file, or can be set via a shell environment variable called `GITLAB_TOKEN` instead. If both are present, the `.gitlab.nvim` file will take precedence.

Optionally provide a GITLAB_URL environment variable (or gitlab_url value in the `.gitlab.nvim` file) to connect to a self-hosted Gitlab instance. This is optional, use ONLY for self-hosted instances.

```
project_id=112415
auth_token=your_gitlab_token
gitlab_url=https://my-personal-gitlab-instance.com/
```

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:
If you don't want to write these into a dotfile, you may provide them via shell variables. These will be overridden by the dotfile if it is present:

```bash
export GITLAB_TOKEN="your_gitlab_token"
export GITLAB_URL="https://my-personal-gitlab-instance.com/"
```

## Configuring the Plugin
Expand Down Expand Up @@ -139,17 +141,17 @@ First, check out the branch that you want to review locally.
git checkout feature-branch
```

Then open Neovim. The `project_id` you specify in your configuration file must match the project_id of the Gitlab project your terminal is inside of.
Then open Neovim. To begin, try running the `summary` command or the `review` command.

### Summary

The `summary` action will pull down the MR description into a buffer so that you can read it. To edit the description, use the `settings.popup.perform_action` keybinding.
The `summary` action will open the MR title and description.

```lua
require("gitlab").summary()
```

The upper part of the popup contains the title, which can also be edited and sent via the perform action keybinding in the same manner.
After editing the description or title, you may save your changes via the `settings.popup.perform_action` keybinding.

### Reviewing Diffs

Expand All @@ -162,7 +164,7 @@ require("gitlab").create_multiline_comment()
require("gitlab").create_comment_suggestion()
```

For suggesting changes you can use `create_comment_suggestion` in visual mode which works similar to `create_multiline_comment` but prefills the comment window with gitlab [suggest changes](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html) code block with prefilled code from visual selection.
For suggesting changes you can use `create_comment_suggestion` in visual mode which works similar to `create_multiline_comment` but prefills the comment window with Gitlab's [suggest changes](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html) code block with prefilled code from the visual selection.

### Discussions and Notes

Expand All @@ -176,7 +178,9 @@ require("gitlab").toggle_discussions()

You can jump to the comment's location in the reviewer window by using the `state.settings.discussion_tree.jump_to_reviewer` key, or the actual file with the 'state.settings.discussion_tree.jump_to_file' key.

Within the discussion tree, you can delete/edit/reply to comments with the `state.settings.discussion_tree.delete_comment` `state.settings.discussion_tree.edit_comment` and `state.settings.discussion_tree.reply` keys, and toggle them as resolved with the `state.settings.discussion_tree.toggle_resolved` key.
Within the discussion tree, you can delete/edit/reply to comments with the `state.settings.discussion_tree.SOME_ACTION` keybindings.

#### Notes

If you'd like to create a note in an MR (like a comment, but not linked to a specific line) use the `create_note` action. The same keybindings for delete/edit/reply are available on the note tree.

Expand Down
162 changes: 93 additions & 69 deletions cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/http/httputil"
"os"
Expand All @@ -18,7 +19,6 @@ type Client struct {
mergeId int
gitlabInstance string
authToken string
logPath string
git *gitlab.Client
}

Expand All @@ -27,85 +27,32 @@ type DebugSettings struct {
GoResponse bool `json:"go_response"`
}

var requestLogger retryablehttp.RequestLogHook = func(l retryablehttp.Logger, r *http.Request, i int) {
logPath := os.Args[len(os.Args)-1]
/* This will parse and validate the project settings and then initialize the Gitlab client */
func (c *Client) initGitlabClient() error {

file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
if len(os.Args) < 6 {
return errors.New("Must provide gitlab url, port, auth token, debug settings, and log path")
}
defer file.Close()

token := r.Header.Get("Private-Token")
r.Header.Set("Private-Token", "REDACTED")
res, err := httputil.DumpRequest(r, true)
if err != nil {
panic(err)
}
r.Header.Set("Private-Token", token)

_, err = file.Write([]byte("\n-- REQUEST --\n")) //nolint:all
_, err = file.Write(res) //nolint:all
_, err = file.Write([]byte("\n")) //nolint:all
}

var responseLogger retryablehttp.ResponseLogHook = func(l retryablehttp.Logger, response *http.Response) {
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)
}
defer file.Close()

res, err := httputil.DumpResponse(response, true)
if err != nil {
panic(err)
gitlabInstance := os.Args[1]
if gitlabInstance == "" {
return errors.New("GitLab instance URL cannot be empty")
}

_, err = file.Write([]byte("\n-- RESPONSE --\n")) //nolint:all
_, err = file.Write(res) //nolint:all
_, err = file.Write([]byte("\n")) //nolint:all
}

/* This will initialize the client with the token and check for the basic project ID and command arguments */
func (c *Client) init(branchName string) error {

if len(os.Args) < 5 {
return errors.New("Must provide project ID, gitlab instance, port, and auth token!")
authToken := os.Args[3]
if authToken == "" {
return errors.New("Auth token cannot be empty")
}

projectId := os.Args[1]
gitlabInstance := os.Args[2]
authToken := os.Args[4]
debugSettings := os.Args[5]

/* Parse debug settings and initialize logger handlers */
debugSettings := os.Args[4]
var debugObject DebugSettings
err := json.Unmarshal([]byte(debugSettings), &debugObject)
if err != nil {
return fmt.Errorf("Could not parse debug settings: %w, %s", err, debugSettings)
}

logPath := os.Args[len(os.Args)-1]

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
c.logPath = logPath

var apiCustUrl = fmt.Sprintf(c.gitlabInstance + "/api/v4")
var apiCustUrl = fmt.Sprintf(gitlabInstance + "/api/v4")

gitlabOptions := []gitlab.ClientOptionFunc{
gitlab.WithBaseURL(apiCustUrl),
Expand All @@ -125,13 +72,40 @@ func (c *Client) init(branchName string) error {
return fmt.Errorf("Failed to create client: %v", err)
}

c.gitlabInstance = gitlabInstance
c.authToken = authToken
c.git = git

return nil
}

/* This will fetch the project ID and merge request ID using the client */
func (c *Client) initProjectSettings(remoteUrl string, namespace string, projectName string, branchName string) error {

idStr := namespace + "/" + projectName
opt := gitlab.GetProjectOptions{}
project, _, err := c.git.Projects.GetProject(idStr, &opt)

if err != nil {
return fmt.Errorf(fmt.Sprintf("Error getting project at %s", remoteUrl), err)
}
if project == nil {
return fmt.Errorf(fmt.Sprintf("Could not find project at %s", remoteUrl), err)
}

if project == nil {
return fmt.Errorf("No projects you are a member of contained remote URL %s", remoteUrl)
}

c.projectId = fmt.Sprint(project.ID)

options := gitlab.ListProjectMergeRequestsOptions{
Scope: gitlab.String("all"),
State: gitlab.String("opened"),
SourceBranch: &branchName,
}

mergeRequests, _, err := git.MergeRequests.ListProjectMergeRequests(c.projectId, &options)
mergeRequests, _, err := c.git.MergeRequests.ListProjectMergeRequests(c.projectId, &options)
if err != nil {
return fmt.Errorf("Failed to list merge requests: %w", err)
}
Expand All @@ -147,7 +121,6 @@ func (c *Client) init(branchName string) error {
}

c.mergeId = mergeIdInt
c.git = git

return nil
}
Expand All @@ -165,3 +138,54 @@ func (c *Client) handleError(w http.ResponseWriter, err error, message string, s
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
}
}

var requestLogger retryablehttp.RequestLogHook = func(l retryablehttp.Logger, r *http.Request, i int) {
file := openLogFile()
defer file.Close()

token := r.Header.Get("Private-Token")
r.Header.Set("Private-Token", "REDACTED")
res, err := httputil.DumpRequest(r, true)
if err != nil {
log.Fatalf("Error dumping request: %v", err)
os.Exit(1)
}
r.Header.Set("Private-Token", token)

_, err = file.Write([]byte("\n-- REQUEST --\n")) //nolint:all
_, err = file.Write(res) //nolint:all
_, err = file.Write([]byte("\n")) //nolint:all
}

var responseLogger retryablehttp.ResponseLogHook = func(l retryablehttp.Logger, response *http.Response) {
file := openLogFile()
defer file.Close()

res, err := httputil.DumpResponse(response, true)
if err != nil {
log.Fatalf("Error dumping response: %v", err)
os.Exit(1)
}

_, err = file.Write([]byte("\n-- RESPONSE --\n")) //nolint:all
_, err = file.Write(res) //nolint:all
_, err = file.Write([]byte("\n")) //nolint:all
}

func openLogFile() *os.File {
logFile := os.Args[len(os.Args)-1]
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
if os.IsNotExist(err) {
log.Printf("Log file %s does not exist", logFile)
} else if os.IsPermission(err) {
log.Printf("Permission denied for log file %s", logFile)
} else {
log.Printf("Error opening log file %s: %v", logFile, err)
}

os.Exit(1)
}

return file
}
67 changes: 67 additions & 0 deletions cmd/git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"fmt"
"os/exec"
"regexp"
"strings"
)

/*
Extracts information about the current repository and returns
it to the client for initialization. The current directory must be a valid
Gitlab project and the branch must be a feature branch
*/
func ExtractGitInfo() (string, string, string, string, error) {

url, err := getProjectUrl()
if err != nil {
return "", "", "", "", fmt.Errorf("Could not get project Url: %v", err)
}

re := regexp.MustCompile(`(?:[:\/])([^\/]+)\/([^\/]+)\.git$`)
matches := re.FindStringSubmatch(url)
if len(matches) != 3 {
return "", "", "", "", fmt.Errorf("Invalid Git URL format: %s", url)
}

namespace := matches[1]
projectName := matches[2]

branch, err := getCurrentBranch()
if err != nil {
return "", "", "", "", fmt.Errorf("Failed to get current branch: %v", err)
}

return url, namespace, projectName, branch, nil
}

/* Gets the current branch */
func getCurrentBranch() (res string, e error) {
gitCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")

output, err := gitCmd.Output()
if err != nil {
return "", fmt.Errorf("Error running git rev-parse: %w", err)
}

branchName := strings.TrimSpace(string(output))

if branchName == "main" || branchName == "master" {
return "", fmt.Errorf("Cannot run on %s branch", branchName)
}

return branchName, nil
}

/* Gets the project SSH or HTTPS url */
func getProjectUrl() (res string, e error) {
cmd := exec.Command("git", "remote", "get-url", "origin")
url, err := cmd.Output()

if err != nil {
return "", fmt.Errorf("Could not get origin remote")
}

return strings.TrimSpace(string(url)), nil
}
Loading