Skip to content

Commit 7b9e4d8

Browse files
committed
Add ff_only parameter to POST /repos/{owner}/{repo}/merge-upstream
The merge-upstream route was so far performing any kind of merge, even those that would create merge commits and thus make your branch diverge from upstream, requiring manual intervention via the git cli to undo the damage. With the new optional parameter ff_only, we can instruct gitea to error out, if a non-fast-forward merge would be performed.
1 parent 6708343 commit 7b9e4d8

File tree

6 files changed

+67
-3
lines changed

6 files changed

+67
-3
lines changed

modules/structs/repo_branch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ type UpdateBranchProtectionPriories struct {
136136

137137
type MergeUpstreamRequest struct {
138138
Branch string `json:"branch"`
139+
FfOnly bool `json:"ff_only"`
139140
}
140141

141142
type MergeUpstreamResponse struct {

routers/api/v1/repo/branch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,7 @@ func MergeUpstream(ctx *context.APIContext) {
11811181
// "404":
11821182
// "$ref": "#/responses/notFound"
11831183
form := web.GetForm(ctx).(*api.MergeUpstreamRequest)
1184-
mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch)
1184+
mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly)
11851185
if err != nil {
11861186
if errors.Is(err, util.ErrInvalidArgument) {
11871187
ctx.APIError(http.StatusBadRequest, err)

routers/web/repo/branch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ func CreateBranch(ctx *context.Context) {
258258

259259
func MergeUpstream(ctx *context.Context) {
260260
branchName := ctx.FormString("branch")
261-
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName)
261+
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false)
262262
if err != nil {
263263
if errors.Is(err, util.ErrNotExist) {
264264
ctx.JSONErrorNotFound()

services/repository/merge_upstream.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
)
1919

2020
// MergeUpstream merges the base repository's default branch into the fork repository's current branch.
21-
func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
21+
func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string, ffOnly bool) (mergeStyle string, err error) {
2222
if err = repo.MustNotBeArchived(); err != nil {
2323
return "", err
2424
}
@@ -45,6 +45,11 @@ func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_
4545
return "", err
4646
}
4747

48+
// If ff_only is requested and fast-forward failed, return error
49+
if ffOnly {
50+
return "", util.NewInvalidArgumentErrorf("fast-forward merge not possible: branch has diverged")
51+
}
52+
4853
// TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment
4954
// ideally in the future the "merge" functions should be refactored to decouple from the PullRequest
5055
fakeIssue := &issue_model.Issue{

templates/swagger/v1_json.tmpl

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/repo_merge_upstream_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,5 +147,59 @@ func TestRepoMergeUpstream(t *testing.T) {
147147
return queryMergeUpstreamButtonLink(htmlDoc) == ""
148148
}, 5*time.Second, 100*time.Millisecond)
149149
})
150+
151+
t.Run("FastForwardOnly", func(t *testing.T) {
152+
// Create a clean branch for fast-forward testing
153+
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{
154+
"_csrf": GetUserCSRFToken(t, session),
155+
"new_branch_name": "ff-test-branch",
156+
})
157+
session.MakeRequest(t, req, http.StatusSeeOther)
158+
159+
// Add content to base repository that can be fast-forwarded
160+
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "ff-test.txt", "master", "ff-content-1"))
161+
162+
// ff_only=true with fast-forward possible (should succeed)
163+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
164+
Branch: "ff-test-branch",
165+
FfOnly: true,
166+
}).AddTokenAuth(token)
167+
resp := MakeRequest(t, req, http.StatusOK)
168+
169+
var mergeResp api.MergeUpstreamResponse
170+
DecodeJSON(t, resp, &mergeResp)
171+
assert.Equal(t, "fast-forward", mergeResp.MergeStyle)
172+
173+
// Test ff_only=true when fast-forward is not possible (using fork-branch which has diverged)
174+
// The fork-branch already has merge commits from previous tests, so this should fail
175+
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "another-file.txt", "master", "more-content"))
176+
177+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
178+
Branch: "fork-branch",
179+
FfOnly: true,
180+
}).AddTokenAuth(token)
181+
MakeRequest(t, req, http.StatusBadRequest)
182+
183+
// same scenario but with ff_only=false (should succeed with merge)
184+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
185+
Branch: "fork-branch",
186+
FfOnly: false,
187+
}).AddTokenAuth(token)
188+
resp = MakeRequest(t, req, http.StatusOK)
189+
190+
DecodeJSON(t, resp, &mergeResp)
191+
assert.Equal(t, "merge", mergeResp.MergeStyle)
192+
193+
// ff_only not specified (should use default behavior - merge)
194+
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "final-file.txt", "master", "final-content"))
195+
196+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
197+
Branch: "fork-branch",
198+
}).AddTokenAuth(token)
199+
resp = MakeRequest(t, req, http.StatusOK)
200+
201+
DecodeJSON(t, resp, &mergeResp)
202+
assert.Equal(t, "merge", mergeResp.MergeStyle)
203+
})
150204
})
151205
}

0 commit comments

Comments
 (0)