Rebased all commits, v0.0.1

added a new file

First major commit

Successful POST of new comment 🚀

Updated README

Updated README 📕

Added more infrastructure

Creating async job

More setup

Getting arguments from Neovim -> Lua -> Golang

Moved commands

Added getProjectInfo command

Adding make comment command

Setting up arguments for MakeComment command

Removed extraneous comments

Setup basic function for adding comments

Lint fixes

Handling bad requests correctly

Better formatting

Printing success message

Adding utility table print

Set comment from popup UI

Added mappings for closing and sending text

Moved popup into separate file

Added comment

Cleaned up code and added approve command

Initialize project information

Removed extraneous import

Don't initialize project in non-gitlab directories

Setup approve command

Set up revoke and approve commands correctly

Cleaned up redundant code

Moved get current branch command

Reorganization of the code

First attempt to add step installing Go binary

Adjusted path to binary

Added install bin check

Do Lua method

Fixed install step

Tweaked binPath + bin

Added basic readme information 📗

Removed .luarc.json file

Adding diffview command

Added string_starts function

Made base branch configurable

Added note to readme

Fixed readme

Added diffview dep to readme

Update README.md

Update README.md

Update README.md

Update README.md

Renamed files

Set up developer workflow

Updated README

Removed dev note

Refactor and moving around files

Fixed ft/after mappings

Setup read command

Added read summary command

Got rid of filetype bindings and set up commands

Set correct filetype for comment buffer

Added read() command to README

Updated review -> summary

Fixed issue with diffview buffers

Added command for getting and showing all comments (out of order)

Better error message

Adding more code to handle showing comments

Added ability to jump from comment to specific changed buffer line

Initial refactor

Added simple comment action

Fixed error message

More cleanup

Fixed bug with M.PROJECT_ID

Leaving comment refactor

Fixed comment

Cleaned up old code

Added missing exit command

Check gitlab repo status before initialization

Better help strings

Added ListDiscussions command

Added Go code

Darkened metadata, filtered out non-real discussions

Removed dummy log

Sort the discussions by most recent activity

Grab hash of current discussion

Wired up reply action in Lua code

Moved to NUI Table

Adding basic jump-to-file ability

More tweaks

Allow jump anywhere in the tree

Ability to reply directly in the buffer window

Jump to location in file

Don't jump if no refresh is set

Cleaned up mappings + other code

Get rid of gitlab CLI dependency

Fixed discussions bug

Don't initialize client on main/master branches

Moved comment into separate module

Moved lua modules into separate files

Modularized library and state

Slightly better error/exception handling

Added license file

Updated readme

Moved into todo.md file

Added todo file

Standardized naming conventions (snake_case in Lua, camelCase in Go)

Moved common popup state into utils folder

Cleaned up keymapping functions

Note on install

Changing bin path

Updated README

Chnaged from success to info

Redirect output to /dev/null on build

Checking install code

Removed print statement

Slight reorganization

Setting up delete comment

Set up confirmation modal

Passing in node ID to delete_comment

Functioning comment deletion

Added delete_comment command

Updated README

Furhter modularized discussion code

Cleaned up and refactored reply code

Update README.md

Added ability to edit comments

Updated todos

Fixed main/master base branch issue

Set up keybinding rules

Updated todo.md

Removed diffview dependency

Slight cleanup 🧹

Trying something out...

Trying something for the binary...

Trying again

Fixed install for non-lazy users

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md
This commit is contained in:
Harrison Cramer
2022-10-22 19:28:19 +00:00
commit bf37b1eae7
27 changed files with 1614 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
bin
.luarc.json

21
LICENSE.txt Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright 2023 Harrison Cramer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

171
README.md Normal file
View File

@@ -0,0 +1,171 @@
# gitlab.nvim
This Neovim plugin is designed to make it easy to review Gitlab MRs from within the editor. This means you can do things like:
- Create, edit, and delete comments on an MR
- Reply to exisiting comments
- Read MR summaries
- Approve an MR
- Revoke approval for an MR
https://user-images.githubusercontent.com/32515581/233739969-216dad6e-fa77-417f-9d2d-5e875ab2fb40.mp4
## Requirements
- Go
- <a href="https://github.com/MunifTanjim/nui.nvim">nui.nvim</a>
- <a href="https://github.com/rcarriga/nvim-notify">nvim-notify</a>
## 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>:
```lua
return {
"harrisoncramer/gitlab.nvim",
dependencies = {
"rcarriga/nvim-notify",
"MunifTanjim/nui.nvim"
},
config = function()
local gitlab = require("gitlab")
gitlab.setup({ project_id = 3 }) -- This can be found under the project details section of your Gitlab repository.
end,
}
```
By default, the tool will look for and interact with MRs against a "main" branch. You can configure this by passing in the `base_branch` option:
```lua
require('gitlab').setup({ project_id = 3, base_branch = 'master' })
```
The first time you call the setup function the Go binary will be built.
## Usage
First, check out the branch that you want to review locally.
The `summary` command will pull down the MR description into a buffer so that you can read it:
```lua
require("gitlab").summary()
```
The `approve` command will approve the merge request for the current branch:
```lua
require("gitlab").approve()
```
The `revoke` command will revoke approval for the merge request for the current branch:
```lua
require("gitlab").revoke()
```
The `comment` command will open up a NUI popover that will allow you to create a Gitlab comment on the current line. To send the comment, use `<leader>s` while the comment popup is open:
```lua
require("gitlab").comment()
```
### Discussions
Gitlab groups threads of notes together into "disucssions." To get a list of all the discussions for the current MR, use the `list_discussions` command. This command will open up a split view of all the comments on the current merge request. You can jump to the comment location by using the `o` key in the tree buffer, and you can reply to a thread by using the `r` keybinding in the tree buffer:
```lua
require("gitlab").list_discussions()
```
Within the discussion tree, there are several functions that you can call, however, it's better to use the keybindings provided in the setup function. If you want to call them manually, they are:
```lua
require("gitlab").delete_comment()
require("gitlab").edit_comment()
require("gitlab").reply()
```
## Keybindings
This plugin does not create any keybindings outside of the plugin-specific buffers by default. These are the default keybindings in those plugin buffers:
```lua
{
popup = { -- The popup for comment creation, editing, and replying
exit = "<Esc>",
perform_action = "<leader>s", -- Once in normal mode, does action
},
discussion_tree = { -- The discussion tree that holds all comments
jump_to_location = "o",
edit_comment = "e",
delete_comment = "dd",
reply_to_comment = "r",
toggle_node = "t",
},
dialogue = { -- The confirmation dialogue for deleting comments
focus_next = { "j", "<Down>", "<Tab>" },
focus_prev = { "k", "<Up>", "<S-Tab>" },
close = { "<Esc>", "<C-c>" },
submit = { "<CR>", "<Space>" },
}
}
```
To override the defaults, pass a keymaps table into the setup function with any keybindings you'd like to change:
```lua
local gitlab = require("gitlab")
gitlab.setup({
project_id = 36091024,
keymaps = {
popup = {
exit = "q"
},
discussion_tree = {
reply_to_comment = "<leader>r"
}
}
})
```
The plugin does not set up any keybindings outside of these buffers, you need to set them up yourself. Here's what I'm using:
```lua
local gitlab = require("gitlab")
vim.keymap.set("n", "<leader>gls", gitlab.summary)
vim.keymap.set("n", "<leader>glA", gitlab.approve)
vim.keymap.set("n", "<leader>glR", gitlab.revoke)
vim.keymap.set("n", "<leader>glc", gitlab.create_comment)
vim.keymap.set("n", "<leader>gld", gitlab.list_discussions)
```
## Diff Views
This plugin does not provide you with a diff view out of the box for viewing changes. That is already handled by other plugins. I highly recommend using Diffview to see which files have changed in an MR. This is the function that I'm using to accomplish this:
```lua
-- Review changes against develop (will break if no develop branch present)
vim.keymap.set("n", "<leader>gR", function()
local isDiff = vim.fn.getwinvar(nil, "&diff")
local bufName = vim.api.nvim_buf_get_name(0)
if isDiff ~= 0 or u.string_starts(bufName, "diff") then
vim.cmd.tabclose()
vim.cmd.tabprev()
else
vim.cmd.DiffviewOpen("main")
end
end)
```
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.

18
cmd/approve.go Normal file
View File

@@ -0,0 +1,18 @@
package main
import (
"fmt"
)
func (c *Client) Approve() error {
_, _, err := c.git.MergeRequestApprovals.ApproveMergeRequest(c.projectId, c.mergeId, nil, nil)
if err != nil {
return fmt.Errorf("Approving MR failed: %w", err)
}
fmt.Println("Success! Approved MR.")
return nil
}

68
cmd/client.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import (
"errors"
"fmt"
"os"
"strconv"
"github.com/xanzy/go-gitlab"
)
type Client struct {
command string
projectId string
mergeId int
git *gitlab.Client
}
/* This will initialize the client with the token and check for the basic project ID and command arguments */
func (c *Client) Init(branchName string) error {
if len(os.Args) < 3 {
return errors.New("Must provide command and projectId")
}
command, projectId := os.Args[1], os.Args[2]
c.command = command
c.projectId = projectId
if projectId == "" {
return errors.New("Must provide projectId")
}
git, err := gitlab.NewClient(os.Getenv("GITLAB_TOKEN"))
if err != nil {
return fmt.Errorf("Failed to create client: %v", err)
}
options := gitlab.ListMergeRequestsOptions{
State: gitlab.String("opened"),
SourceBranch: &branchName,
}
mergeRequests, _, err := git.MergeRequests.ListMergeRequests(&options)
if err != nil {
return fmt.Errorf("Failed to list merge requests: %w", err)
}
if len(mergeRequests) == 0 {
return errors.New("No merge requests found")
}
mergeId := strconv.Itoa(mergeRequests[0].IID)
mergeIdInt, err := strconv.Atoi(mergeId)
if err != nil {
return err
}
c.mergeId = mergeIdInt
c.git = git
return nil
}
func (c *Client) Usage(command string) {
fmt.Printf("Usage: gitlab-nvim %s <project-id> ...args", command)
os.Exit(1)
}

116
cmd/comment.go Normal file
View File

@@ -0,0 +1,116 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/xanzy/go-gitlab"
)
const mrVersionsUrl = "https://gitlab.com/api/v4/projects/%s/merge_requests/%d/versions"
type MRVersion struct {
ID int `json:"id"`
HeadCommitSHA string `json:"head_commit_sha"`
BaseCommitSHA string `json:"base_commit_sha"`
StartCommitSHA string `json:"start_commit_sha"`
CreatedAt time.Time `json:"created_at"`
MergeRequestID int `json:"merge_request_id"`
State string `json:"state"`
RealSize string `json:"real_size"`
}
func (c *Client) Comment() error {
if len(os.Args) < 6 {
c.Usage("comment")
}
lineNumber, fileName, comment := os.Args[3], os.Args[4], os.Args[5]
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)
}
err, response := getMRVersions(c.projectId, c.mergeId)
if err != nil {
log.Fatalf("Error making diff thread: %s", err)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
var diffVersionInfo []MRVersion
err = json.Unmarshal(body, &diffVersionInfo)
time := time.Now()
options := gitlab.CreateMergeRequestDiscussionOptions{
Body: &comment,
CreatedAt: &time,
Position: &gitlab.NotePosition{
PositionType: "text",
BaseSHA: diffVersionInfo[0].BaseCommitSHA,
HeadSHA: diffVersionInfo[0].HeadCommitSHA,
StartSHA: diffVersionInfo[0].StartCommitSHA,
NewPath: fileName,
OldPath: fileName,
NewLine: lineNumberInt,
},
}
_, _, err = c.git.Discussions.CreateMergeRequestDiscussion(c.projectId, c.mergeId, &options)
if err != nil {
return fmt.Errorf("Could not leave comment: %w", err)
}
fmt.Println("Left Comment: " + comment[0:min(len(comment), 25)] + "...")
return nil
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}
/* 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)
req, err := http.NewRequest(http.MethodGet, url, nil)
req.Header.Add("PRIVATE-TOKEN", gitlabToken)
if err != nil {
return err, nil
}
client := &http.Client{}
response, err = client.Do(req)
if err != nil {
return err, nil
}
if response.StatusCode != 200 {
return errors.New("Non-200 status code: " + response.Status), nil
}
return nil, response
}

29
cmd/delete_comment.go Normal file
View File

@@ -0,0 +1,29 @@
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
}

46
cmd/edit_comment.go Normal file
View File

@@ -0,0 +1,46 @@
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
}

46
cmd/info.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
"errors"
"fmt"
"io"
"net/http"
"os"
)
const mrUrl = "https://gitlab.com/api/v4/projects/%s/merge_requests/%d"
func (c *Client) Info() error {
url := fmt.Sprintf(mrUrl, c.projectId, c.mergeId)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("Failed to build read request: %w", err)
}
req.Header.Set("PRIVATE-TOKEN", os.Getenv("GITLAB_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to make info request: %w", err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
return errors.New(fmt.Sprintf("Recieved non-200 response: %d", res.StatusCode))
}
body, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("Failed to parse read response: %w", err)
}
/* This response is parsed into a table in our Lua code */
fmt.Println(string(body))
return nil
}

60
cmd/list_discussions.go Normal file
View File

@@ -0,0 +1,60 @@
package main
import (
"fmt"
"sort"
"encoding/json"
"github.com/xanzy/go-gitlab"
)
type SortableDiscussions []*gitlab.Discussion
func (n SortableDiscussions) Len() int {
return len(n)
}
func (d SortableDiscussions) Less(i int, j int) bool {
iTime := d[i].Notes[len(d[i].Notes)-1].CreatedAt
jTime := d[j].Notes[len(d[j].Notes)-1].CreatedAt
return iTime.After(*jTime)
}
func (n SortableDiscussions) Swap(i, j int) {
n[i], n[j] = n[j], n[i]
}
func (c *Client) ListDiscussions() error {
mergeRequestDiscussionOptions := gitlab.ListMergeRequestDiscussionsOptions{
Page: 1,
PerPage: 250,
}
discussions, _, err := c.git.Discussions.ListMergeRequestDiscussions(c.projectId, c.mergeId, &mergeRequestDiscussionOptions, nil)
if err != nil {
return fmt.Errorf("Listing discussions failed: %w", err)
}
var realDiscussions []*gitlab.Discussion
for i := 0; i < len(discussions); i++ {
notes := discussions[i].Notes
for j := 0; j < len(notes); j++ {
if notes[j].Type == gitlab.NoteTypeValue("DiffNote") {
realDiscussions = append(realDiscussions, discussions[i])
break
}
}
}
sortedDiscussions := SortableDiscussions(realDiscussions)
sort.Sort(sortedDiscussions)
discussionsOutput, err := json.Marshal(sortedDiscussions)
fmt.Println(string(discussionsOutput))
return nil
}

76
cmd/main.go Normal file
View File

@@ -0,0 +1,76 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
)
const (
star = "star"
info = "info"
approve = "approve"
revoke = "revoke"
comment = "comment"
deleteComment = "deleteComment"
editComment = "editComment"
reply = "reply"
listDiscussions = "listDiscussions"
)
func main() {
branchName, err := getCurrentBranch()
errCheck(err)
if branchName == "main" || branchName == "master" {
return
}
var c Client
errCheck(c.Init(branchName))
switch c.command {
case star:
errCheck(c.Star())
case approve:
errCheck(c.Approve())
case revoke:
errCheck(c.Revoke())
case comment:
errCheck(c.Comment())
case deleteComment:
errCheck(c.DeleteComment())
case editComment:
errCheck(c.EditComment())
case info:
errCheck(c.Info())
case reply:
errCheck(c.Reply())
case listDiscussions:
errCheck(c.ListDiscussions())
default:
c.Usage("command")
}
}
func errCheck(err error) {
if err != nil {
log.Fatalf("Failure: %s", err)
os.Exit(1)
}
}
/* 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)
}
return strings.TrimSpace(string(output)), nil
}

41
cmd/reply.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"time"
"github.com/xanzy/go-gitlab"
)
func (c *Client) Reply() error {
if len(os.Args) < 5 {
return errors.New("Must provide discussionId and reply text")
}
discussionId, reply := os.Args[3], os.Args[4]
now := time.Now()
options := gitlab.AddMergeRequestDiscussionNoteOptions{
Body: gitlab.String(reply),
CreatedAt: &now,
}
gitlabNote, _, err := c.git.Discussions.AddMergeRequestDiscussionNote(c.projectId, c.mergeId, discussionId, &options)
if err != nil {
return fmt.Errorf("Could not leave reply: %w", err)
}
output, err := json.Marshal(gitlabNote)
if err != nil {
return fmt.Errorf("Could not marshal note: %w", err)
}
fmt.Println(string(output))
return nil
}

18
cmd/revoke.go Normal file
View File

@@ -0,0 +1,18 @@
package main
import (
"fmt"
)
func (c *Client) Revoke() error {
_, err := c.git.MergeRequestApprovals.UnapproveMergeRequest(c.projectId, c.mergeId, nil, nil)
if err != nil {
return fmt.Errorf("Revoking approval failed: %w", err)
}
fmt.Println("Success! Revoked MR approval.")
return nil
}

17
cmd/star.go Normal file
View File

@@ -0,0 +1,17 @@
package main
import (
"fmt"
"log"
)
func (c *Client) Star() error {
project, _, err := c.git.Projects.StarProject(c.projectId)
if err != nil {
return fmt.Errorf("Starring project failed: %w", err)
}
log.Printf("Success! Starred project: %s", project.Name)
return nil
}

17
go.mod Normal file
View File

@@ -0,0 +1,17 @@
module gitlab.com/harrisoncramer/gitlab.nvim
go 1.19
require github.com/xanzy/go-gitlab v0.83.0
require (
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.29.1 // indirect
)

43
go.sum Normal file
View File

@@ -0,0 +1,43 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/xanzy/go-gitlab v0.83.0 h1:37p0MpTPNbsTMKX/JnmJtY8Ch1sFiJzVF342+RvZEGw=
github.com/xanzy/go-gitlab v0.83.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

18
lua/gitlab/approve.lua Normal file
View File

@@ -0,0 +1,18 @@
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

187
lua/gitlab/comment.lua Normal file
View File

@@ -0,0 +1,187 @@
local Menu = require("nui.menu")
local NuiTree = require("nui.tree")
local Job = require("plenary.job")
local state = require("gitlab.state")
local u = require("gitlab.utils")
local keymaps = require("gitlab.keymaps")
local Popup = require("nui.popup")
local M = {}
local commentPopup = Popup(u.create_popup_state("Comment", "40%", "60%"))
local editPopup = Popup(u.create_popup_state("Edit Comment", "80%", "80%"))
M.create_comment = function()
if u.base_invalid() then return end
commentPopup:mount()
keymaps.set_popup_keymaps(commentPopup, M.confirm_create_comment)
end
-- Sends the comment to Gitlab
M.confirm_create_comment = function(text)
if u.base_invalid() then return end
local relative_file_path = u.get_relative_file_path()
local current_line_number = u.get_current_line_number()
Job:new({
command = state.BIN,
args = {
"comment",
state.PROJECT_ID,
current_line_number,
relative_file_path,
text,
},
-- TODO: Render the tree after comment creation. Refresh?
on_stdout = u.print_success,
on_stderr = u.print_error
}):start()
end
M.delete_comment = function()
local menu = Menu({
position = "50%",
size = {
width = 25,
},
border = {
style = "single",
text = {
top = "Delete Comment?",
top_align = "center",
},
},
win_options = {
winhighlight = "Normal:Normal,FloatBorder:Normal",
},
}, {
lines = {
Menu.item("Confirm"),
Menu.item("Cancel"),
},
max_width = 20,
keymap = {
focus_next = state.keymaps.dialogue.focus_next,
focus_prev = state.keymaps.dialogue.focus_prev,
close = state.keymaps.dialogue.close,
submit = state.keymaps.dialogue.submit,
},
on_submit = function(item)
if item.text == "Confirm" then
local note_id
local node = state.tree:get_node()
if node.is_note then
note_id = node:get_id()
end
local parentId = node:get_parent_id()
while (parentId ~= nil) do
node = state.tree:get_node(parentId)
parentId = node:get_parent_id()
if node.is_note then
note_id = node:get_id()
end
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
Job:new({
command = state.BIN,
args = {
"deleteComment",
state.PROJECT_ID,
discussion_id,
note_id,
},
on_stdout = function(_, line)
vim.schedule(function()
if line ~= nil and line ~= "" then
require("notify")(line, "info")
state.tree:remove_node("-" .. note_id)
local discussion_node = state.tree:get_node("-" .. discussion_id)
if not discussion_node:has_children() then
state.tree:remove_node("-" .. discussion_id)
end
state.tree:render()
end
end)
end,
on_stderr = u.print_error
}):start()
end
end,
})
menu:mount()
end
M.edit_comment = function()
if u.base_invalid() then return end
local node = state.tree:get_node()
if node.is_discussion then return end
if node.is_body then
local parentId = node:get_parent_id()
node = state.tree:get_node(parentId) -- Get the node for the comment
end
editPopup:mount()
local note_id = 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
state.ACTIVE_DISCUSSION = discussion_id
state.ACTIVE_NOTE = note_id
local lines = {}
local childrenIds = node:get_child_ids()
for _, value in ipairs(childrenIds) do
local line = state.tree:get_node(value).text
table.insert(lines, line)
end
local currentBuffer = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
keymaps.set_popup_keymaps(editPopup, M.send_edits)
end
M.send_edits = function(text)
Job:new({
command = state.BIN,
args = {
"editComment",
state.PROJECT_ID,
state.ACTIVE_DISCUSSION,
state.ACTIVE_NOTE,
text,
},
on_stdout = function(_, line)
local note = vim.json.decode(line)
if note == nil then
require("notify")("There was an issue editing the note", "error")
return
end
vim.schedule(function()
local node = state.tree:get_node("-" .. state.ACTIVE_NOTE)
local childrenIds = node:get_child_ids()
for _, value in ipairs(childrenIds) do
state.tree:remove_node(value)
end
local newNoteTextNodes = {}
for bodyLine in note.body:gmatch("[^\n]+") do
table.insert(newNoteTextNodes, NuiTree.Node({ text = bodyLine, is_body = true }, {}))
end
state.tree:set_nodes(newNoteTextNodes, "-" .. state.ACTIVE_NOTE)
state.tree:render()
local buf = vim.api.nvim_get_current_buf()
u.darken_metadata(buf, '')
require("notify")("Edited comment!")
end)
end,
on_stderr = u.print_error
}):start()
end
return M

207
lua/gitlab/discussions.lua Normal file
View File

@@ -0,0 +1,207 @@
local u = require("gitlab.utils")
local NuiTree = require("nui.tree")
local state = require("gitlab.state")
local Job = require("plenary.job")
local Popup = require("nui.popup")
local keymaps = require("gitlab.keymaps")
local M = {}
local replyPopup = Popup(u.create_popup_state("Reply", "80%", "80%"))
M.reply = function()
if u.base_invalid() then return end
replyPopup:mount()
keymaps.set_popup_keymaps(replyPopup, M.send_reply)
end
M.send_reply = function(text)
Job:new({
command = state.BIN,
args = {
"reply",
state.PROJECT_ID,
state.ACTIVE_DISCUSSION,
text,
},
on_stdout = function(_, line)
local note = vim.json.decode(line)
if note == nil then
require("notify")("There was an issue creating the note", "error")
return
end
local note_node = M.build_note(note)
note_node:expand()
state.tree:add_node(note_node, "-" .. state.ACTIVE_DISCUSSION)
vim.schedule(function()
state.tree:render()
local buf = vim.api.nvim_get_current_buf()
u.darken_metadata(buf, '')
require("notify")("Sent reply!")
end)
end,
on_stderr = u.print_error
}):start()
end
-- Places all of the discussions into a readable list
M.list_discussions = function()
if u.base_invalid() then return end
Job:new({
command = state.BIN,
args = { "listDiscussions", state.PROJECT_ID },
on_stdout = function(_, line)
local discussions = vim.json.decode(line)
M.discussions = discussions
vim.schedule(function()
vim.cmd.tabnew()
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_command("vsplit")
vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown')
vim.api.nvim_set_current_buf(buf)
if discussions == nil then
require("notify")("No discussions found for this MR", "warn")
else
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
state.tree = NuiTree({ nodes = allDiscussions, bufnr = buf })
M.set_tree_keymaps(buf)
state.tree:render()
vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown')
u.darken_metadata(buf, '')
if not is_refresh then
M.jump_to_file()
end
end
end)
end,
on_stderr = u.print_error,
}):start()
end
M.jump_to_file = function()
local node = state.tree:get_node()
if node == nil then return end
local childrenIds = node:get_child_ids()
-- We have selected a note node
if node.file_name ~= nil then
u.jump_to_file(node.file_name, node.line_number)
elseif node.is_body then
local parentId = node:get_parent_id()
local parent = state.tree:get_node(parentId)
if parent == nil then return end
u.jump_to_file(parent.file_name, parent.line_number)
else
local firstChild = state.tree:get_node(childrenIds[1])
if firstChild == nil then return end
u.jump_to_file(firstChild.file_name, firstChild.line_number)
end
end
M.set_tree_keymaps = function(buf)
-- Jump to file location where comment was left
vim.keymap.set('n', state.keymaps.discussion_tree.jump_to_location, function()
M.jump_to_file()
end, { buffer = true })
vim.keymap.set('n', state.keymaps.discussion_tree.edit_comment, function()
require("gitlab.comment").edit_comment()
end, { buffer = true })
vim.keymap.set('n', state.keymaps.discussion_tree.delete_comment, function()
require("gitlab.comment").delete_comment()
end)
-- Expand/collapse the current node
vim.keymap.set('n', state.keymaps.discussion_tree.toggle_node, function()
local node = state.tree:get_node()
if node == nil then return end
local children = node:get_child_ids()
if node == nil then return end
if node:is_expanded() then
node:collapse()
for _, child in ipairs(children) do
state.tree:get_node(child):collapse()
end
else
for _, child in ipairs(children) do
state.tree:get_node(child):expand()
end
node:expand()
end
state.tree:render()
u.darken_metadata(buf, '')
end,
{ buffer = true })
vim.keymap.set('n', 'r', function()
local node = state.tree:get_node()
if node == nil then return end
-- Get closest discussion parent
if node.is_body then
local parentId = node:get_parent_id()
local parent = state.tree:get_node(parentId)
if parent == nil then return end
parentId = parent:get_parent_id()
parent = state.tree:get_node(parentId)
if parent == nil then return end
node = parent
elseif node.is_note then
local parentId = node:get_parent_id()
local parent = state.tree:get_node(parentId)
if parent == nil then return end
node = parent
end
state.ACTIVE_DISCUSSION = node.id
M.reply()
end, { buffer = true })
end
M.build_note = function(note)
local noteTextNodes = {}
for bodyLine in note.body:gmatch("[^\n]+") do
table.insert(noteTextNodes, NuiTree.Node({ text = bodyLine, is_body = true }, {}))
end
local noteHeader = "@" ..
note.author.username .. " on " .. u.format_date(note.created_at)
local note_node = NuiTree.Node(
{
text = noteHeader,
id = note.id,
file_name = note.position.new_path,
line_number = note.position.new_line,
is_note = true
}, noteTextNodes)
return note_node
end
return M

79
lua/gitlab/init.lua Normal file
View File

@@ -0,0 +1,79 @@
local Job = require("plenary.job")
local state = require("gitlab.state")
local discussions = require("gitlab.discussions")
local summary = require("gitlab.summary")
local keymaps = require("gitlab.keymaps")
local comment = require("gitlab.comment")
local approve = require("gitlab.approve")
local revoke = require("gitlab.revoke")
local u = require("gitlab.utils")
-- Root Module Scope
local M = {}
M.summary = summary.summary
M.approve = approve.approve
M.revoke = revoke.revoke
M.create_comment = comment.create_comment
M.list_discussions = discussions.list_discussions
M.edit_comment = comment.edit_comment
M.delete_comment = comment.delete_comment
M.reply = discussions.reply
-- Builds the Go binary, initializes the plugin, fetches MR info
local projectData = {}
local function current_file_path()
local path = debug.getinfo(1, 'S').source:sub(2)
return vim.fn.fnamemodify(path, ':p')
end
M.setup = function(args)
local file_path = current_file_path()
local parent_dir = vim.fn.fnamemodify(file_path, ":h:h:h")
state.BIN_PATH = parent_dir
state.BIN = parent_dir .. "/bin"
local binExists = io.open(state.BIN, "r")
if not binExists or args.dev == true then
local command = string.format("cd %s && make", state.BIN_PATH)
local installCode = os.execute(command .. "> /dev/null")
if installCode ~= 0 then
require("notify")("Could not install gitlab.nvim! Do you have Go installed?", "error")
return
end
end
if args.project_id == nil then
error("No project ID provided!")
end
state.PROJECT_ID = args.project_id
if args.base_branch ~= nil then
state.BASE_BRANCH = args.base_branch
end
if u.is_gitlab_repo() then
Job:new({
command = state.BIN,
args = { "info", state.PROJECT_ID },
on_stdout = function(_, line)
table.insert(projectData, line)
end,
on_stderr = u.print_error,
on_exit = function()
if projectData[1] ~= nil then
local parsed_ok, data = pcall(vim.json.decode, projectData[1])
if parsed_ok ~= true then
require("notify")("Failed calling setup. Could not get project data.", "error")
else
state.INFO = data
end
end
end,
}):start()
end
keymaps.set_keymap_keys(args.keymaps)
end
return M

22
lua/gitlab/keymaps.lua Normal file
View File

@@ -0,0 +1,22 @@
local u = require("gitlab.utils")
local state = require("gitlab.state")
local M = {}
M.set_popup_keymaps = function(popup, action)
vim.keymap.set('n', state.keymaps.popup.exit, function() u.exit(popup) end, { buffer = true })
vim.keymap.set('n', ':', '', { buffer = true })
if action ~= nil then
vim.keymap.set('n', state.keymaps.popup.perform_action, function()
local text = u.get_buffer_text(popup.bufnr)
popup:unmount()
action(text)
end, { buffer = true })
end
end
M.set_keymap_keys = function(keyTable)
if keyTable == nil then return end
state.keymaps = u.merge_tables(state.keymaps, keyTable)
end
return M

17
lua/gitlab/revoke.lua Normal file
View File

@@ -0,0 +1,17 @@
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

29
lua/gitlab/state.lua Normal file
View File

@@ -0,0 +1,29 @@
local M = {}
M.BIN_PATH = nil
M.BIN = nil
M.PROJECT_ID = nil
M.ACTIVE_DISCUSSION = nil
M.ACTIVE_NOTE = nil
M.BASE_BRANCH = "main"
M.keymaps = {
popup = {
exit = "<Esc>",
perform_action = "<leader>s",
},
discussion_tree = {
jump_to_location = "o",
edit_comment = "e",
delete_comment = "dd",
reply_to_comment = "r",
toggle_node = "t",
},
dialogue = {
focus_next = { "j", "<Down>", "<Tab>" },
focus_prev = { "k", "<Up>", "<S-Tab>" },
close = { "<Esc>", "<C-c>" },
submit = { "<CR>", "<Space>" },
}
}
return M

27
lua/gitlab/summary.lua Normal file
View File

@@ -0,0 +1,27 @@
local state = require("gitlab.state")
local Popup = require("nui.popup")
local u = require("gitlab.utils")
local keymaps = require("gitlab.keymaps")
local summaryPopup = Popup(u.create_popup_state("Loading Summary...", "80%", "80%"))
local M = {}
M.summary = function()
if u.base_invalid() then return end
summaryPopup:mount()
local currentBuffer = vim.api.nvim_get_current_buf()
local title = state.INFO.title
local description = state.INFO.description
local lines = {}
for line in description:gmatch("[^\n]+") do
table.insert(lines, line)
table.insert(lines, "")
end
vim.schedule(function()
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
vim.api.nvim_buf_set_option(currentBuffer, "modifiable", false)
summaryPopup.border:set_text("top", title, "center")
keymaps.set_popup_keymaps(summaryPopup)
end)
end
return M

226
lua/gitlab/utils/init.lua Normal file
View File

@@ -0,0 +1,226 @@
local state = require("gitlab.state")
local function get_git_root()
local output = vim.fn.system('git rev-parse --show-toplevel 2>/dev/null')
if vim.v.shell_error == 0 then
return vim.fn.substitute(output, '\n', '', '')
else
return nil
end
end
local function get_relative_file_path()
local git_root = get_git_root()
if git_root ~= nil then
local current_file = vim.fn.expand('%:p')
return vim.fn.substitute(current_file, git_root .. '/', '', '')
else
return nil
end
end
local get_current_line_number = function()
return vim.api.nvim_call_function('line', { '.' })
end
function P(...)
local objects = {}
for i = 1, select("#", ...) do
local v = select(i, ...)
table.insert(objects, vim.inspect(v))
end
print(table.concat(objects, "\n"))
return ...
end
local function get_buffer_text(bufnr)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local text = table.concat(lines, "\n")
return text
end
local feature_branch_exists = function(base_branch)
local is_git_branch = io.popen("git rev-parse --is-inside-work-tree 2>/dev/null"):read("*a")
if is_git_branch == "true\n" then
for line in io.popen("git branch 2>/dev/null"):lines() do
line = line:gsub("%s+", "")
if line == base_branch then
return true
end
end
end
return false
end
local string_starts = function(str, start)
return str:sub(1, #start) == start
end
local press_enter = function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<CR>", false, true, true), "n", false)
end
local base_invalid = function()
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 == "main" or current_branch == "master" then
require("notify")('On ' .. current_branch .. ' branch, no MRs available', "error")
return true
end
local base = state.BASE_BRANCH
local hasBaseBranch = feature_branch_exists(base)
if not hasBaseBranch then
require("notify")('No base branch. If this is a Gitlab repository, please check your setup function!', "error")
return true
end
end
local format_date = function(date_string)
local year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
local date = os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
-- Format date into human-readable string without leading zeros
local formatted_date = os.date("%A, %B %e at %l:%M %p", date)
return formatted_date
end
local add_comment_sign = function(line_number)
local bufnr = vim.api.nvim_get_current_buf()
vim.cmd("sign define piet text= texthl=Substitute")
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)
vim.api.nvim_command("wincmd l")
local bufnr = vim.fn.bufnr(filename)
if bufnr ~= -1 then
-- Buffer is already open, switch to it
vim.cmd("buffer " .. bufnr)
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
return
end
-- If buffer is not already open, open it
vim.cmd("edit " .. filename)
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
end
local function find_value_by_id(tbl, id)
for i = 1, #tbl do
if tbl[i].id == tonumber(id) then
return tbl[i]
end
end
return nil
end
vim.cmd("highlight Gray guifg=#888888")
local function darken_metadata(bufnr, regex)
local num_lines = vim.api.nvim_buf_line_count(bufnr)
for i = 0, num_lines - 1 do
local line = vim.api.nvim_buf_get_lines(bufnr, i, i + 1, false)[1]
if string.match(line, regex) then
vim.api.nvim_buf_add_highlight(bufnr, -1, 'Gray', i, 0, -1)
end
end
end
local function print_success(_, line)
if line ~= nil and line ~= "" then
require("notify")(line, "info")
end
end
local function print_error(_, line)
if line ~= nil and line ~= "" then
require("notify")(line, "error")
end
end
local function exit(popup)
popup:unmount()
end
local create_popup_state = function(title, width, height)
return {
buf_options = {
filetype = 'markdown'
},
enter = true,
focusable = true,
border = {
style = "rounded",
text = {
top = title
},
},
position = "50%",
size = {
width = width,
height = height,
},
}
end
local M = {}
M.merge_tables = function(defaults, overrides)
local result = {}
for key, value in pairs(defaults) do
if type(value) == "table" then
result[key] = M.merge_tables(value, overrides[key] or {})
else
result[key] = overrides[key] or value
end
end
return result
end
M.get_relative_file_path = get_relative_file_path
M.get_current_line_number = get_current_line_number
M.get_buffer_text = get_buffer_text
M.feature_branch_exists = feature_branch_exists
M.press_enter = press_enter
M.string_starts = string_starts
M.base_invalid = base_invalid
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
M.create_popup_state = create_popup_state
M.exit = exit
M.P = P
return M

2
makefile Normal file
View File

@@ -0,0 +1,2 @@
compile:
cd cmd && go build -o bin && mv bin ../bin

11
todo.md Normal file
View File

@@ -0,0 +1,11 @@
## Todo
- [x] Install for non-Lazy users
- [ ] Fix comments on non-changed file lines
- [x] Delete, Edit Comments
- [ ] Auto-Pick buffer when Cycling Through Comments
- [x] Clean up Camel Case to Snake Case
- [x] Common Popup State
- [x] Silence make command on build (don't rebuild every time)
- [x] Fix gitlab.nvim vs. gitlab namespacing issue
- [x] Allow customization of keybindings