Skip to content

Commit 679d2ad

Browse files
committed
feat: implement rebasing
1 parent 3d2828a commit 679d2ad

8 files changed

Lines changed: 229 additions & 0 deletions

File tree

cmd/app/rebase_mr.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package app
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
gitlab "gitlab.com/gitlab-org/api/client-go"
9+
)
10+
11+
type RebaseMrRequest struct {
12+
SkipCI bool `json:"skip_ci,omitempty"`
13+
}
14+
15+
type MergeRequestRebaser interface {
16+
RebaseMergeRequest(pid interface{}, mergeRequest int64, opt *gitlab.RebaseMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error)
17+
}
18+
19+
type mergeRequestRebaserService struct {
20+
data
21+
client MergeRequestRebaser
22+
}
23+
24+
/* Rebases a merge request on the server */
25+
func (a mergeRequestRebaserService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
26+
27+
payload := r.Context().Value(payload("payload")).(*RebaseMrRequest)
28+
29+
opts := gitlab.RebaseMergeRequestOptions{
30+
SkipCI: &payload.SkipCI,
31+
}
32+
33+
res, err := a.client.RebaseMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &opts)
34+
35+
if err != nil {
36+
handleError(w, err, "Could not rebase MR", http.StatusInternalServerError)
37+
return
38+
}
39+
40+
if res.StatusCode >= 300 {
41+
handleError(w, GenericError{r.URL.Path}, "Could not rebase MR", res.StatusCode)
42+
return
43+
}
44+
45+
skippingCI := ""
46+
if payload.SkipCI {
47+
skippingCI = " (skipping CI)"
48+
}
49+
response := SuccessResponse{Message: fmt.Sprintf("MR rebased successfully%s", skippingCI)}
50+
51+
w.WriteHeader(http.StatusOK)
52+
53+
err = json.NewEncoder(w).Encode(response)
54+
if err != nil {
55+
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
56+
}
57+
}

cmd/app/rebase_mr_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package app
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
gitlab "gitlab.com/gitlab-org/api/client-go"
8+
)
9+
10+
type fakeMergeRequestRebaser struct {
11+
testBase
12+
}
13+
14+
func (f fakeMergeRequestRebaser) RebaseMergeRequest(pid interface{}, mergeRequest int64, opt *gitlab.RebaseMergeRequestOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
15+
resp, err := f.handleGitlabError()
16+
if err != nil {
17+
return nil, err
18+
}
19+
20+
return resp, err
21+
}
22+
23+
func TestRebaseHandler(t *testing.T) {
24+
var testRebaseMrPayload = RebaseMrRequest{SkipCI: false}
25+
t.Run("Rebases merge request", func(t *testing.T) {
26+
request := makeRequest(t, http.MethodPost, "/mr/rebase", testRebaseMrPayload)
27+
svc := middleware(
28+
mergeRequestRebaserService{testProjectData, fakeMergeRequestRebaser{}},
29+
withMr(testProjectData, fakeMergeRequestLister{}),
30+
withPayloadValidation(methodToPayload{
31+
http.MethodPost: newPayload[RebaseMrRequest],
32+
}),
33+
withMethodCheck(http.MethodPost),
34+
)
35+
data := getSuccessData(t, svc, request)
36+
assert(t, data.Message, "MR rebased successfully")
37+
})
38+
var testRebaseMrPayloadSkipCI = RebaseMrRequest{SkipCI: true}
39+
t.Run("Rebases merge request and skips CI", func(t *testing.T) {
40+
request := makeRequest(t, http.MethodPost, "/mr/rebase", testRebaseMrPayloadSkipCI)
41+
svc := middleware(
42+
mergeRequestRebaserService{testProjectData, fakeMergeRequestRebaser{}},
43+
withMr(testProjectData, fakeMergeRequestLister{}),
44+
withPayloadValidation(methodToPayload{
45+
http.MethodPost: newPayload[RebaseMrRequest],
46+
}),
47+
withMethodCheck(http.MethodPost),
48+
)
49+
data := getSuccessData(t, svc, request)
50+
assert(t, data.Message, "MR rebased successfully (skipping CI)")
51+
})
52+
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
53+
request := makeRequest(t, http.MethodPost, "/mr/rebase", testRebaseMrPayload)
54+
svc := middleware(
55+
mergeRequestRebaserService{testProjectData, fakeMergeRequestRebaser{testBase{errFromGitlab: true}}},
56+
withMr(testProjectData, fakeMergeRequestLister{}),
57+
withPayloadValidation(methodToPayload{
58+
http.MethodPost: newPayload[RebaseMrRequest],
59+
}),
60+
withMethodCheck(http.MethodPost),
61+
)
62+
data, _ := getFailData(t, svc, request)
63+
checkErrorFromGitlab(t, data, "Could not rebase MR")
64+
})
65+
t.Run("Handles non-200s from Gitlab", func(t *testing.T) {
66+
request := makeRequest(t, http.MethodPost, "/mr/rebase", testRebaseMrPayload)
67+
svc := middleware(
68+
mergeRequestRebaserService{testProjectData, fakeMergeRequestRebaser{testBase{status: http.StatusSeeOther}}},
69+
withMr(testProjectData, fakeMergeRequestLister{}),
70+
withPayloadValidation(methodToPayload{
71+
http.MethodPost: newPayload[RebaseMrRequest],
72+
}),
73+
withMethodCheck(http.MethodPost),
74+
)
75+
data, _ := getFailData(t, svc, request)
76+
checkNon200(t, data, "Could not rebase MR", "/mr/rebase")
77+
})
78+
}

cmd/app/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ func CreateRouter(gitlabClient *Client, projectInfo *ProjectInfo, s *shutdownSer
117117
withPayloadValidation(methodToPayload{http.MethodPost: newPayload[AcceptMergeRequestRequest]}),
118118
withMethodCheck(http.MethodPost),
119119
))
120+
m.HandleFunc("/mr/rebase", middleware(
121+
mergeRequestRebaserService{d, gitlabClient},
122+
withMr(d, gitlabClient),
123+
withPayloadValidation(methodToPayload{http.MethodPost: newPayload[RebaseMrRequest]}),
124+
withMethodCheck(http.MethodPost),
125+
))
120126
m.HandleFunc("/mr/discussions/list", middleware(
121127
discussionsListerService{d, gitlabClient},
122128
withMr(d, gitlabClient),

doc/gitlab.nvim.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ you call this function with no values the defaults will be used:
179179
approve = "glA", -- Approve MR
180180
revoke = "glR", -- Revoke MR approval
181181
merge = "glM", -- Merge the feature branch to the target branch and close MR
182+
rebase = "glrr", -- Rebase the feature branch of the MR on the server and pull the new state
183+
rebase_skip_ci = "glrs", -- Same as `rebase`, but skip the CI pipeline
182184
create_mr = "glC", -- Create a new MR for currently checked-out feature branch
183185
choose_merge_request = "glc", -- Chose MR for review (if necessary check out the feature branch)
184186
start_review = "glS", -- Start review for the currently checked-out branch
@@ -1045,6 +1047,20 @@ Gitlab online. You can see the current settings in the Summary view, see
10451047
Use the `keymaps.popup.perform_action` to merge the MR
10461048
with your message.
10471049

1050+
*gitlab.nvim.rebase*
1051+
gitlab.rebase({opts}) ~
1052+
1053+
Rebases the feature branch of the MR on the server and pulls the new state of
1054+
the target branch.
1055+
>lua
1056+
require("gitlab").rebase()
1057+
require("gitlab").rebase({ skip_ci = true })
1058+
<
1059+
Parameters: ~
1060+
{opts}: (table|nil) Keyword arguments that can be used to override
1061+
default behavior.
1062+
• {skip_ci}: (bool) If true, the CI pipeline will be skipped.
1063+
10481064
*gitlab.nvim.data*
10491065
gitlab.data({resources}, {cb}) ~
10501066

lua/gitlab/actions/rebase.lua

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
local u = require("gitlab.utils")
2+
3+
local M = {}
4+
5+
---@class RebaseOpts
6+
---@field skip_ci boolean?
7+
8+
local can_rebase = function()
9+
local git = require("gitlab.git")
10+
-- Check if there are local changes (couldn't run `git pull` after rebasing)
11+
local has_clean_tree, err = git.has_clean_tree()
12+
if not has_clean_tree then
13+
u.notify("Cannot rebase when there are changed files", vim.log.levels.ERROR)
14+
return false
15+
elseif err ~= nil then
16+
u.notify("Error while inspecting working tree", vim.log.levels.ERROR)
17+
return false
18+
end
19+
return true
20+
end
21+
22+
---@param opts RebaseOpts
23+
M.rebase = function(opts)
24+
if not can_rebase() then
25+
return
26+
end
27+
28+
-- TODO: check that MR needs rebasing (requires https://github.com/harrisoncramer/gitlab.nvim/pull/532)
29+
30+
local state = require("gitlab.state")
31+
local rebase_body = { skip_ci = state.settings.rebase_mr.skip_ci }
32+
if opts and opts.skip_ci ~= nil then
33+
rebase_body.skip_ci = opts.skip_ci
34+
end
35+
36+
M.confirm_rebase(rebase_body)
37+
end
38+
39+
---@param merge_body RebaseOpts
40+
M.confirm_rebase = function(merge_body)
41+
local job = require("gitlab.job")
42+
job.run_job("/mr/rebase", "POST", merge_body, function(data)
43+
u.notify(data.message, vim.log.levels.INFO)
44+
u.notify("Implement pulling", vim.log.levels.INFO)
45+
u.notify("Implement updating the reviewer", vim.log.levels.INFO)
46+
end)
47+
end
48+
49+
return M

lua/gitlab/annotations.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
---@field discussion_signs? DiscussionSigns -- The settings for discussion signs/diagnostics
163163
---@field pipeline? PipelineSettings -- The settings for the pipeline popup
164164
---@field create_mr? CreateMrSettings -- The settings when creating an MR
165+
---@field rebase_mr? RebaseMrSettings -- The settings when rebasing an MR
165166
---@field colors? ColorSettings --- Colors settings for the plugin
166167

167168
---@class DiscussionSigns: table
@@ -196,6 +197,9 @@
196197
---@field title_input? TitleInputSettings
197198
---@field fork? ForkSettings
198199

200+
---@class RebaseMrSettings: table
201+
---@field skip_ci? boolean -- Whether to skip CI after rabasing
202+
199203
---@class ForkSettings: table
200204
---@field enabled? boolean -- If making an MR from a fork
201205
---@field forked_project_id? number -- The Gitlab ID of the project you are merging into. If nil, will be prompted.

lua/gitlab/init.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ local reviewer = require("gitlab.reviewer")
88
local discussions = require("gitlab.actions.discussions")
99
local merge_requests = require("gitlab.actions.merge_requests")
1010
local merge = require("gitlab.actions.merge")
11+
local rebase = require("gitlab.actions.rebase")
1112
local summary = require("gitlab.actions.summary")
1213
local data = require("gitlab.actions.data")
1314
local assignees_and_reviewers = require("gitlab.actions.assignees_and_reviewers")
@@ -86,6 +87,7 @@ return {
8687
end,
8788
pipeline = async.sequence({ latest_pipeline }, pipeline.open),
8889
merge = async.sequence({ u.merge(info, { refresh = true }) }, merge.merge),
90+
rebase = async.sequence({ u.merge(info, { refresh = true }) }, rebase.rebase),
8991
-- Discussion Tree Actions 🌴
9092
toggle_discussions = function()
9193
if discussions.split_visible then

lua/gitlab/state.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ M.settings = {
8181
approve = "glA",
8282
revoke = "glR",
8383
merge = "glM",
84+
rebase = "glrr",
85+
rebase_skip_ci = "glrs",
8486
create_mr = "glC",
8587
choose_merge_request = "glc",
8688
start_review = "glS",
@@ -196,6 +198,9 @@ M.settings = {
196198
border = "rounded",
197199
},
198200
},
201+
rebase_mr = {
202+
skip_ci = false,
203+
},
199204
choose_merge_request = {
200205
open_reviewer = true,
201206
},
@@ -389,6 +394,18 @@ M.set_global_keymaps = function()
389394
end, { desc = "Merge MR", nowait = keymaps.global.merge_nowait })
390395
end
391396

397+
if keymaps.global.rebase then
398+
vim.keymap.set("n", keymaps.global.rebase, function()
399+
require("gitlab").rebase()
400+
end, { desc = "Rebase MR", nowait = keymaps.global.rebase_nowait })
401+
end
402+
403+
if keymaps.global.rebase_skip_ci then
404+
vim.keymap.set("n", keymaps.global.rebase_skip_ci, function()
405+
require("gitlab").rebase({ skip_ci = true })
406+
end, { desc = "Rebase MR and skip CI", nowait = keymaps.global.rebase_skip_ci_nowait })
407+
end
408+
392409
if keymaps.global.copy_mr_url then
393410
vim.keymap.set("n", keymaps.global.copy_mr_url, function()
394411
require("gitlab").copy_mr_url()

0 commit comments

Comments
 (0)