Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ glrd Delete reviewer
glA Approve MR
glR Revoke MR approval
glM Merge the feature branch to the target branch and close MR
glrr Rebase the feature branch of the MR on the server and pull the new state
glrs Same as `glrr`, but skip the CI pipeline
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
57 changes: 57 additions & 0 deletions cmd/app/rebase_mr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package app

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

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

type RebaseMrRequest struct {
SkipCI bool `json:"skip_ci,omitempty"`
}

type MergeRequestRebaser interface {
RebaseMergeRequest(pid interface{}, mergeRequest int64, opt *gitlab.RebaseMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
}

type mergeRequestRebaserService struct {
data
client MergeRequestRebaser
}

/* Rebases a merge request on the server */
func (a mergeRequestRebaserService) ServeHTTP(w http.ResponseWriter, r *http.Request) {

payload := r.Context().Value(payload("payload")).(*RebaseMrRequest)

opts := gitlab.RebaseMergeRequestOptions{
SkipCI: &payload.SkipCI,
}

res, err := a.client.RebaseMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opts)

if err != nil {
handleError(w, err, "Could not rebase MR", http.StatusInternalServerError)
return
}

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

skippingCI := ""
if payload.SkipCI {
skippingCI = " (skipping CI)"
}
response := SuccessResponse{Message: fmt.Sprintf("MR rebased successfully%s", skippingCI)}

w.WriteHeader(http.StatusOK)

err = json.NewEncoder(w).Encode(response)
if err != nil {
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
}
}
78 changes: 78 additions & 0 deletions cmd/app/rebase_mr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package app

import (
"net/http"
"testing"

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

type fakeMergeRequestRebaser struct {
testBase
}

func (f fakeMergeRequestRebaser) RebaseMergeRequest(pid interface{}, mergeRequest int64, opt *gitlab.RebaseMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
resp, err := f.handleGitlabError()
if err != nil {
return nil, err
}

return resp, err
}

func TestRebaseHandler(t *testing.T) {
var testRebaseMrPayload = RebaseMrRequest{SkipCI: false}
t.Run("Rebases merge request", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/rebase", testRebaseMrPayload)
svc := middleware(
mergeRequestRebaserService{testProjectData, fakeMergeRequestRebaser{}},
withMr(testProjectData, fakeMergeRequestLister{}),
withPayloadValidation(methodToPayload{
http.MethodPost: newPayload[RebaseMrRequest],
}),
withMethodCheck(http.MethodPost),
)
data := getSuccessData(t, svc, request)
assert(t, data.Message, "MR rebased successfully")
})
var testRebaseMrPayloadSkipCI = RebaseMrRequest{SkipCI: true}
t.Run("Rebases merge request and skips CI", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/rebase", testRebaseMrPayloadSkipCI)
svc := middleware(
mergeRequestRebaserService{testProjectData, fakeMergeRequestRebaser{}},
withMr(testProjectData, fakeMergeRequestLister{}),
withPayloadValidation(methodToPayload{
http.MethodPost: newPayload[RebaseMrRequest],
}),
withMethodCheck(http.MethodPost),
)
data := getSuccessData(t, svc, request)
assert(t, data.Message, "MR rebased successfully (skipping CI)")
})
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/rebase", testRebaseMrPayload)
svc := middleware(
mergeRequestRebaserService{testProjectData, fakeMergeRequestRebaser{testBase{errFromGitlab: true}}},
withMr(testProjectData, fakeMergeRequestLister{}),
withPayloadValidation(methodToPayload{
http.MethodPost: newPayload[RebaseMrRequest],
}),
withMethodCheck(http.MethodPost),
)
data, _ := getFailData(t, svc, request)
checkErrorFromGitlab(t, data, "Could not rebase MR")
})
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
request := makeRequest(t, http.MethodPost, "/mr/rebase", testRebaseMrPayload)
svc := middleware(
mergeRequestRebaserService{testProjectData, fakeMergeRequestRebaser{testBase{status: http.StatusSeeOther}}},
withMr(testProjectData, fakeMergeRequestLister{}),
withPayloadValidation(methodToPayload{
http.MethodPost: newPayload[RebaseMrRequest],
}),
withMethodCheck(http.MethodPost),
)
data, _ := getFailData(t, svc, request)
checkNon200(t, data, "Could not rebase MR", "/mr/rebase")
})
}
6 changes: 6 additions & 0 deletions cmd/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ func CreateRouter(gitlabClient *Client, projectInfo *ProjectInfo, s *shutdownSer
withPayloadValidation(methodToPayload{http.MethodPost: newPayload[AcceptMergeRequestRequest]}),
withMethodCheck(http.MethodPost),
))
m.HandleFunc("/mr/rebase", middleware(
mergeRequestRebaserService{d, gitlabClient},
withMr(d, gitlabClient),
withPayloadValidation(methodToPayload{http.MethodPost: newPayload[RebaseMrRequest]}),
withMethodCheck(http.MethodPost),
))
m.HandleFunc("/mr/discussions/list", middleware(
discussionsListerService{d, gitlabClient},
withMr(d, gitlabClient),
Expand Down
16 changes: 16 additions & 0 deletions doc/gitlab.nvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ you call this function with no values the defaults will be used:
approve = "glA", -- Approve MR
revoke = "glR", -- Revoke MR approval
merge = "glM", -- Merge the feature branch to the target branch and close MR
rebase = "glrr", -- Rebase the feature branch of the MR on the server and pull the new state
rebase_skip_ci = "glrs", -- Same as `rebase`, but skip the CI pipeline
create_mr = "glC", -- Create a new MR for currently checked-out feature branch
choose_merge_request = "glc", -- Chose MR for review (if necessary check out the feature branch)
start_review = "glS", -- Start review for the currently checked-out branch
Expand Down Expand Up @@ -1045,6 +1047,20 @@ Gitlab online. You can see the current settings in the Summary view, see
Use the `keymaps.popup.perform_action` to merge the MR
with your message.

*gitlab.nvim.rebase*
gitlab.rebase({opts}) ~

Rebases the feature branch of the MR on the server and pulls the new state of
the target branch.
>lua
require("gitlab").rebase()
require("gitlab").rebase({ skip_ci = true })
<
Parameters: ~
• {opts}: (table|nil) Keyword arguments that can be used to override
default behavior.
• {skip_ci}: (bool) If true, the CI pipeline will be skipped.

*gitlab.nvim.data*
gitlab.data({resources}, {cb}) ~

Expand Down
60 changes: 60 additions & 0 deletions lua/gitlab/actions/rebase.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
local u = require("gitlab.utils")

local M = {}

---@class RebaseOpts
---@field skip_ci boolean?

local can_rebase = function()
local git = require("gitlab.git")
-- Check if there are local changes (we wouldn't be able to run `git pull` after rebasing)
local has_clean_tree, err = git.has_clean_tree()
if not has_clean_tree then
u.notify("Cannot rebase when there are changed files", vim.log.levels.ERROR)
return false
elseif err ~= nil then
u.notify("Error while inspecting working tree", vim.log.levels.ERROR)
return false
end
return true
end

---@param opts RebaseOpts
M.rebase = function(opts)
if not can_rebase() then
return
end

-- TODO: check that MR needs rebasing (requires https://github.com/harrisoncramer/gitlab.nvim/pull/532)

local state = require("gitlab.state")
local rebase_body = { skip_ci = state.settings.rebase_mr.skip_ci }
if opts and opts.skip_ci ~= nil then
rebase_body.skip_ci = opts.skip_ci
end

M.confirm_rebase(rebase_body)
end

---@param merge_body RebaseOpts
M.confirm_rebase = function(merge_body)
local job = require("gitlab.job")
job.run_job("/mr/rebase", "POST", merge_body, function(data)
u.notify(data.message, vim.log.levels.INFO)
local git = require("gitlab.git")
local state = require("gitlab.state")
local success = git.pull(state.settings.connection_settings.remote, state.INFO.source_branch, { "--rebase" })
if success then
u.notify(
string.format(
"Pulled `%s %s` successfully",
state.settings.connection_settings.remote,
state.INFO.source_branch
),
vim.log.levels.INFO
)
end
end)
end

return M
4 changes: 4 additions & 0 deletions lua/gitlab/annotations.lua
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
---@field discussion_signs? DiscussionSigns -- The settings for discussion signs/diagnostics
---@field pipeline? PipelineSettings -- The settings for the pipeline popup
---@field create_mr? CreateMrSettings -- The settings when creating an MR
---@field rebase_mr? RebaseMrSettings -- The settings when rebasing an MR
---@field colors? ColorSettings --- Colors settings for the plugin

---@class DiscussionSigns: table
Expand Down Expand Up @@ -196,6 +197,9 @@
---@field title_input? TitleInputSettings
---@field fork? ForkSettings

---@class RebaseMrSettings: table
---@field skip_ci? boolean -- Whether to skip CI after rabasing

---@class ForkSettings: table
---@field enabled? boolean -- If making an MR from a fork
---@field forked_project_id? number -- The Gitlab ID of the project you are merging into. If nil, will be prompted.
Expand Down
28 changes: 28 additions & 0 deletions lua/gitlab/git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,34 @@ M.get_ahead_behind = function(current_branch, remote_branch, log_level)
return true -- Checks passed, branch is up-to-date
end

---Pull the branch from remote
---@param remote string
---@param branch string
---@param opts string[]?
---@return boolean success True if the branch has been pulled successfully
M.pull = function(remote, branch, opts)
local current_branch = M.get_current_branch()
if not current_branch then
return false
end
if current_branch ~= branch then
local u = require("gitlab.utils")
u.notify("Cannot pull. Remote branch is not the same as current branch", vim.log.levels.ERROR)
return false
end
local args = { "git", "pull" }
for _, opt in ipairs(opts or {}) do
table.insert(args, opt)
end
table.insert(args, remote)
table.insert(args, branch)
local _, err = run_system(args)
if err ~= nil then
return false
end
return true
end

---Return the name of the current branch or nil if it can't be retrieved
---@return string|nil
M.get_current_branch = function()
Expand Down
2 changes: 2 additions & 0 deletions lua/gitlab/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ local reviewer = require("gitlab.reviewer")
local discussions = require("gitlab.actions.discussions")
local merge_requests = require("gitlab.actions.merge_requests")
local merge = require("gitlab.actions.merge")
local rebase = require("gitlab.actions.rebase")
local summary = require("gitlab.actions.summary")
local data = require("gitlab.actions.data")
local assignees_and_reviewers = require("gitlab.actions.assignees_and_reviewers")
Expand Down Expand Up @@ -86,6 +87,7 @@ return {
end,
pipeline = async.sequence({ latest_pipeline }, pipeline.open),
merge = async.sequence({ u.merge(info, { refresh = true }) }, merge.merge),
rebase = async.sequence({ u.merge(info, { refresh = true }) }, rebase.rebase),
-- Discussion Tree Actions 🌴
toggle_discussions = function()
if discussions.split_visible then
Expand Down
25 changes: 13 additions & 12 deletions lua/gitlab/reviewer/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
local List = require("gitlab.utils.list")
local u = require("gitlab.utils")
local state = require("gitlab.state")
local git = require("gitlab.git")
local hunks = require("gitlab.hunks")
local async = require("diffview.async")
local diffview_lib = require("diffview.lib")
Expand All @@ -29,18 +28,16 @@ M.init = function()
end
end

-- Opens the reviewer window.
-- Opens the reviewer windows.
M.open = function()
local diff_refs = state.INFO.diff_refs
if diff_refs == nil then
u.notify("Gitlab did not provide diff refs required to review this MR", vim.log.levels.ERROR)
return
end
local git = require("gitlab.git")

if diff_refs.base_sha == "" or diff_refs.head_sha == "" then
u.notify("Merge request contains no changes", vim.log.levels.ERROR)
local remote_target_branch =
string.format("%s/%s", state.settings.connection_settings.remote, state.INFO.target_branch)
if not git.fetch_remote_branch(remote_target_branch) then
return
end
git.check_current_branch_up_to_date_on_remote(vim.log.levels.WARN)

local diffview_open_command = "DiffviewOpen"

Expand All @@ -53,17 +50,22 @@ M.open = function()
diffview_open_command = diffview_open_command .. " --imply-local"
else
u.notify(
"Your working tree has changes, cannot use 'imply_local' setting for gitlab reviews.\n Stash or commit all changes to use.",
"Working tree unclean, cannot use 'imply_local' for review. Stash or commit all changes to use.",
vim.log.levels.WARN
)
state.settings.reviewer_settings.diffview.imply_local = false
end
end

vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
local full_command = string.format("%s %s..%s", diffview_open_command, remote_target_branch, state.INFO.source_branch)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By using branch names to open the Diffview the view automatically updates after fetching the remote target branch and pulling the source branch which has been rebased on the server.

I still need to find out if this doesn't break anything or if the diff_refs should not be replaced in other parts of the codebase as well.

vim.api.nvim_command(full_command)

M.is_open = true
local cur_view = diffview_lib.get_current_view()
if cur_view == nil then
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
return
end
M.diffview_layout = cur_view.cur_layout
M.tabnr = vim.api.nvim_get_current_tabpage()

Expand Down Expand Up @@ -94,7 +96,6 @@ M.open = function()
require("gitlab").toggle_discussions() -- Fetches data and opens discussions
end

git.check_current_branch_up_to_date_on_remote(vim.log.levels.WARN)
git.check_mr_in_good_condition()
end

Expand Down
Loading
Loading