Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
d26ab84
fix: revert removing on_error_callback
jakubbortlik Feb 6, 2026
d71bdbb
fix: allow publishing drafts when local not in sync with remote
jakubbortlik Feb 10, 2026
f62bf65
fix: check window valid when refocusing after closing popup
jakubbortlik Feb 11, 2026
129b83b
feat: implement pagination for MR discussions
jakubbortlik Feb 17, 2026
946b718
perf: only fetch emojis for filtered discussions
jakubbortlik Feb 17, 2026
250ba35
feat: add mergeability checks to summary view
jakubbortlik Feb 26, 2026
d193bcd
test: add error return value check
jakubbortlik Feb 26, 2026
5069212
docs: add missing value in fields annotation
jakubbortlik Feb 26, 2026
d7ddf1c
fix: add nil check
jakubbortlik Feb 26, 2026
c2e76d0
fix: highlight individual lines
jakubbortlik Feb 27, 2026
8b282b7
fix: use head_pipeline if available
jakubbortlik Feb 27, 2026
d67484f
fix: check local branch up-to-date on remote before creating comment
jakubbortlik Feb 28, 2026
f7c7a93
feat: show ahead and behind commits in winbar
jakubbortlik Mar 2, 2026
6ed0956
fix: use correct dependency key
jakubbortlik Mar 2, 2026
4d7da05
Merge pull request #532 from jakubbortlik/feat/implement-mergeability…
harrisoncramer Mar 18, 2026
68fabb5
Merge pull request #529 from jakubbortlik/fix/use-pagination-to-list-…
harrisoncramer Mar 18, 2026
9ddf47e
Merge pull request #525 from jakubbortlik/fix/revert-removing-on_erro…
harrisoncramer Mar 18, 2026
c82c667
Merge pull request #527 from jakubbortlik/fix/invalid-window
harrisoncramer Mar 18, 2026
ea8a1c8
Merge pull request #526 from jakubbortlik/fix/allow-publish-drafts-wi…
harrisoncramer Mar 18, 2026
4e0b24f
Merge pull request #533 from jakubbortlik/fix/check-local-in-sync-bef…
harrisoncramer Mar 18, 2026
094738f
Prevent usage of external diff tools
SlayerOfTheBad Mar 25, 2026
0f007fc
Merge pull request #536 from SlayerOfTheBad/no-ext-diff
jakubbortlik Mar 25, 2026
ffe3435
style: fix indentation
jakubbortlik Feb 18, 2026
863e1e7
feat!: implement AutoMerge option to AcceptMergeRequest #518
jakubbortlik Feb 18, 2026
3204338
feat: add default global keymap for auto-merge
jakubbortlik Feb 18, 2026
8fbf5b4
style: apply stylua
jakubbortlik Feb 18, 2026
4b29e57
feat: better handling of server installation path
acristoffers Feb 12, 2026
04c1eae
fix: set LC_TIME when testing, since the test expects it to be English.
acristoffers Feb 13, 2026
750e329
fixup! feat: better handling of server installation path
acristoffers Mar 30, 2026
4b9d3f5
fixup! feat: better handling of server installation path
acristoffers Mar 30, 2026
63e5b95
fixup! feat: better handling of server installation path
acristoffers Mar 31, 2026
d9ed62f
fixup! feat: better handling of server installation path
acristoffers Mar 31, 2026
810f5bd
docs: recommend using dlyongemallo's fork of diffview
jakubbortlik Apr 8, 2026
3018c83
Merge pull request #539 from jakubbortlik/docs/recommend-diffview-fork
harrisoncramer Apr 18, 2026
30ae897
Merge pull request #528 from acristoffers/feature/nix-and-configurabl…
harrisoncramer Apr 18, 2026
5ca461e
fix(build): allow user-supplied binaries
harrisoncramer Apr 18, 2026
d649d83
Merge pull request #530 from jakubbortlik/feat/implement-auto-merge
harrisoncramer Apr 18, 2026
f867aa7
Merge pull request #540 from harrisoncramer/fix/server-build-issues
harrisoncramer Apr 18, 2026
a8a7286
fix: add SECURITY_POLICY_PIPELINE_CHECK mergeability identifier
harrisoncramer Apr 18, 2026
1b0111e
Merge pull request #542 from harrisoncramer/fix/mergeability-check-id…
harrisoncramer Apr 18, 2026
7ad65df
fix(browser): add Windows support for opening URLs
harrisoncramer Apr 18, 2026
2fd25eb
fix(proxy): bypass proxy for localhost requests to Go server
harrisoncramer Apr 18, 2026
4a890ea
formatting
harrisoncramer Apr 18, 2026
e8981eb
Merge pull request #543 from harrisoncramer/fix/windows-open-url
harrisoncramer Apr 18, 2026
418c9c0
Merge pull request #544 from harrisoncramer/fix/proxy-localhost-bypass
harrisoncramer Apr 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/update-gomod2nix.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Update gomod2nix

on:
push:
branches:
- develop
paths:
- go.mod
- go.sum

jobs:
update:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable

- name: Generate gomod2nix.toml
run: nix run github:nix-community/gomod2nix -- generate

- name: Commit updated gomod2nix.toml
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: update gomod2nix.toml"
file_pattern: gomod2nix.toml
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ With <a href="https://github.com/folke/lazy.nvim">Lazy</a>:
dependencies = {
"MunifTanjim/nui.nvim",
"nvim-lua/plenary.nvim",
"sindrets/diffview.nvim",
"dlyongemallo/diffview.nvim", -- Maintained fork of "sindrets/diffview.nvim".
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
},
Expand All @@ -59,7 +59,7 @@ And with <a href="https://github.com/lewis6991/pckr.nvim">pckr.nvim</a>:
requires = {
"MunifTanjim/nui.nvim",
"nvim-lua/plenary.nvim",
"sindrets/diffview.nvim",
"dlyongemallo/diffview.nvim", -- Maintained fork of "sindrets/diffview.nvim".
"stevearc/dressing.nvim", -- Recommended but not required. Better UI for pickers.
"nvim-tree/nvim-web-devicons", -- Recommended but not required. Icons in discussion tree.
},
Expand All @@ -73,6 +73,8 @@ And with <a href="https://github.com/lewis6991/pckr.nvim">pckr.nvim</a>:

Add `branch = "develop",` to your configuration if you want to use the (possibly unstable) development version of `gitlab.nvim`.

`gitlab.nvim` uses the `diffview.nvim` plugin for showing the diffs in a MR. We recommend using [dlyongemallo's](https://github.com/dlyongemallo/diffview.nvim) fork which is the de-facto maintained version of the plugin with many fixes and improvements (e.g., marking files as viewed).

## Contributing

Contributions to the plugin are welcome. Please read [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) before you start working on a pull request.
Expand Down Expand Up @@ -140,6 +142,7 @@ glrd Delete reviewer
glA Approve MR
glR Revoke MR approval
glM Merge the feature branch to the target branch and close MR
glm Set MR to merge automatically when the pipeline succeeds
glC Create a new MR for currently checked-out feature branch
glc Chose MR for review
glS Start review for the currently checked-out branch
Expand Down
2 changes: 2 additions & 0 deletions cmd/app/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Client struct {
gitlab.UsersServiceInterface
gitlab.DraftNotesServiceInterface
gitlab.ProjectMarkdownUploadsServiceInterface
gitlab.GraphQLInterface
}

/* NewClient parses and validates the project settings and initializes the Gitlab client. */
Expand Down Expand Up @@ -100,6 +101,7 @@ func NewClient() (*Client, error) {
client.Users,
client.DraftNotes,
client.ProjectMarkdownUploads,
client.GraphQL,
}, nil
}

Expand Down
15 changes: 7 additions & 8 deletions cmd/app/list_discussions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"net/http"
"slices"
"sort"
"sync"
"time"
Expand Down Expand Up @@ -90,18 +91,16 @@ func (a discussionsListerService) ServeHTTP(w http.ResponseWriter, r *http.Reque
},
}

discussions, res, err := a.client.ListMergeRequestDiscussions(a.projectInfo.ProjectId, a.projectInfo.MergeId, &mergeRequestDiscussionOptions)
it, hasErr := gitlab.Scan(func(p gitlab.PaginationOptionFunc) ([]*gitlab.Discussion, *gitlab.Response, error) {
return a.client.ListMergeRequestDiscussions(a.projectInfo.ProjectId, a.projectInfo.MergeId, &mergeRequestDiscussionOptions, p)
})
discussions := slices.Collect(it)

if err != nil {
if err := hasErr(); err != nil {
handleError(w, err, "Could not list discussions", http.StatusInternalServerError)
return
}

if res.StatusCode >= 300 {
handleError(w, GenericError{r.URL.Path}, "Could not list discussions", res.StatusCode)
return
}

/* Filter out any discussions started by a blacklisted user
and system discussions, then return them sorted by created date */
var unlinkedDiscussions []*gitlab.Discussion
Expand All @@ -124,7 +123,7 @@ func (a discussionsListerService) ServeHTTP(w http.ResponseWriter, r *http.Reque

/* Collect IDs in order to fetch emojis */
var noteIds []int64
for _, discussion := range discussions {
for _, discussion := range slices.Concat(linkedDiscussions, unlinkedDiscussions) {
for _, note := range discussion.Notes {
noteIds = append(noteIds, note.ID)
}
Expand Down
11 changes: 0 additions & 11 deletions cmd/app/list_discussions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,6 @@ func TestListDiscussions(t *testing.T) {
data, _ := getFailData(t, svc, request)
checkErrorFromGitlab(t, data, "Could not list discussions")
})
t.Run("Handles non-200s from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})
svc := middleware(
discussionsListerService{testProjectData, fakeDiscussionsLister{testBase: testBase{status: http.StatusSeeOther}}},
withMr(testProjectData, fakeMergeRequestLister{}),
withPayloadValidation(methodToPayload{http.MethodPost: newPayload[DiscussionsRequest]}),
withMethodCheck(http.MethodPost),
)
data, _ := getFailData(t, svc, request)
checkNon200(t, data, "Could not list discussions", "/mr/discussions/list")
})
t.Run("Handles error from emoji service", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/discussions/list", DiscussionsRequest{Blacklist: []string{}})
svc := middleware(
Expand Down
10 changes: 9 additions & 1 deletion cmd/app/merge_mr.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
)

type AcceptMergeRequestRequest struct {
AutoMerge bool `json:"auto_merge"`
DeleteBranch bool `json:"delete_branch"`
SquashMessage string `json:"squash_message"`
Squash bool `json:"squash"`
Expand All @@ -27,6 +28,7 @@ func (a mergeRequestAccepterService) ServeHTTP(w http.ResponseWriter, r *http.Re
payload := r.Context().Value(payload("payload")).(*AcceptMergeRequestRequest)

opts := gitlab.AcceptMergeRequestOptions{
AutoMerge: &payload.AutoMerge,
Squash: &payload.Squash,
ShouldRemoveSourceBranch: &payload.DeleteBranch,
}
Expand All @@ -47,7 +49,13 @@ func (a mergeRequestAccepterService) ServeHTTP(w http.ResponseWriter, r *http.Re
return
}

response := SuccessResponse{Message: "MR merged successfully"}
var message string
if payload.AutoMerge {
message = "MR set to be merged when all checks pass"
} else {
message = "MR merged successfully"
}
response := SuccessResponse{Message: message}

w.WriteHeader(http.StatusOK)

Expand Down
83 changes: 83 additions & 0 deletions cmd/app/mergeability_checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package app

import (
"encoding/json"
"fmt"
"net/http"

gitlab "gitlab.com/gitlab-org/api/client-go"
)

type MergeabilityCheck struct {
Identifier string `json:"identifier"`
Status string `json:"status"`
}

type MergeabilityChecksResponse struct {
SuccessResponse
MergeabilityChecks []*MergeabilityCheck `json:"mergeability_checks"`
}

type mergeabilityChecksGraphQLResponse struct {
Data struct {
Project struct {
MergeRequest struct {
MergeabilityChecks []*MergeabilityCheck `json:"mergeabilityChecks"`
} `json:"mergeRequest"`
} `json:"project"`
} `json:"data"`
}

const mergeabilityChecksQuery = `
query GetMergeabilityChecks($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
mergeabilityChecks {
identifier
status
}
}
}
}
`

type mergeabilityChecksService struct {
data
client gitlab.GraphQLInterface
}

func (a mergeabilityChecksService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
checks, err := a.fetchMergeabilityChecks()
if err != nil {
handleError(w, err, "Could not get mergeability checks", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
response := MergeabilityChecksResponse{
SuccessResponse: SuccessResponse{Message: "Mergeability checks retrieved"},
MergeabilityChecks: checks,
}

err = json.NewEncoder(w).Encode(response)
if err != nil {
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
}
}

func (a mergeabilityChecksService) fetchMergeabilityChecks() ([]*MergeabilityCheck, error) {
var response mergeabilityChecksGraphQLResponse

_, err := a.client.Do(gitlab.GraphQLQuery{
Query: mergeabilityChecksQuery,
Variables: map[string]any{
"projectPath": a.gitInfo.ProjectPath(),
"iid": fmt.Sprintf("%d", a.projectInfo.MergeId),
},
}, &response)
if err != nil {
return nil, fmt.Errorf("failed to fetch mergeability checks: %w", err)
}

return response.Data.Project.MergeRequest.MergeabilityChecks, nil
}
121 changes: 121 additions & 0 deletions cmd/app/mergeability_checks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package app

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/harrisoncramer/gitlab.nvim/cmd/app/git"
gitlab "gitlab.com/gitlab-org/api/client-go"
)

type fakeGraphQLClient struct {
err error
jsonData []byte
}

func (f fakeGraphQLClient) Do(query gitlab.GraphQLQuery, response any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
if f.err != nil {
return nil, f.err
}

// Actually unmarshal JSON into the response struct
if err := json.Unmarshal(f.jsonData, response); err != nil {
return nil, err
}

// if resp, ok := response.(mergeabilityChecksGraphQLResponse); ok {
// resp.Data.Project.MergeRequest.MergeabilityChecks = f.checks
// }

return makeResponse(http.StatusOK), nil
}

var testMergeabilityData = data{
projectInfo: &ProjectInfo{MergeId: 123},
gitInfo: &git.GitData{
BranchName: "feature-branch",
Namespace: "test-namespace",
ProjectName: "test-project",
},
}

func TestMergeabilityChecksHandler(t *testing.T) {
t.Run("Returns mergeability checks", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/mr/mergeability_checks", nil)
client := fakeGraphQLClient{
jsonData: []byte(`{
"data": {
"project": {
"mergeRequest": {
"mergeabilityChecks": [
{"identifier": "CI_MUST_PASS", "status": "SUCCESS"},
{"identifier": "CONFLICT", "status": "FAILED"}
]
}
}
}
}`),
}
svc := middleware(
mergeabilityChecksService{testMergeabilityData, client},
withMethodCheck(http.MethodGet),
)

res := httptest.NewRecorder()
svc.ServeHTTP(res, request)

var data MergeabilityChecksResponse
err := json.Unmarshal(res.Body.Bytes(), &data)
assert(t, err, nil)

assert(t, data.Message, "Mergeability checks retrieved")
assert(t, len(data.MergeabilityChecks), 2)
assert(t, data.MergeabilityChecks[0].Identifier, "CI_MUST_PASS")
assert(t, data.MergeabilityChecks[0].Status, "SUCCESS")
assert(t, data.MergeabilityChecks[1].Identifier, "CONFLICT")
assert(t, data.MergeabilityChecks[1].Status, "FAILED")
})

t.Run("Returns empty list when there are no checks", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/mr/mergeability_checks", nil)
client := fakeGraphQLClient{
jsonData: []byte(`{
"data": {
"project": {
"mergeRequest": {
"mergeabilityChecks": []
}
}
}
}`),
}
svc := middleware(
mergeabilityChecksService{testMergeabilityData, client},
withMethodCheck(http.MethodGet),
)

res := httptest.NewRecorder()
svc.ServeHTTP(res, request)

var data MergeabilityChecksResponse
err := json.Unmarshal(res.Body.Bytes(), &data)
assert(t, err, nil)

assert(t, data.Message, "Mergeability checks retrieved")
assert(t, len(data.MergeabilityChecks), 0)
})

t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/mr/mergeability_checks", nil)
client := fakeGraphQLClient{err: errorFromGitlab}
svc := middleware(
mergeabilityChecksService{testMergeabilityData, client},
withMethodCheck(http.MethodGet),
)
data, _ := getFailData(t, svc, request)
assert(t, data.Message, "Could not get mergeability checks")
assert(t, data.Details, "failed to fetch mergeability checks: "+errorFromGitlab.Error())
})
}
5 changes: 5 additions & 0 deletions cmd/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ func CreateRouter(gitlabClient *Client, projectInfo *ProjectInfo, s *shutdownSer
withMr(d, gitlabClient),
withMethodCheck(http.MethodGet),
))
m.HandleFunc("/mr/info/mergeability", middleware(
mergeabilityChecksService{d, gitlabClient},
withMr(d, gitlabClient),
withMethodCheck(http.MethodGet),
))
m.HandleFunc("/mr/assignee", middleware(
assigneesService{d, gitlabClient},
withMr(d, gitlabClient),
Expand Down
Loading
Loading