From b8c9a0c323dd8945128acec6b0b9af0452f59ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Thu, 19 Jun 2025 21:29:10 +0200 Subject: [PATCH] Add ff_only parameter to POST /repos/{owner}/{repo}/merge-upstream (#34770) 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. --- modules/structs/repo_branch.go | 1 + routers/api/v1/repo/branch.go | 2 +- routers/web/repo/branch.go | 2 +- services/repository/merge_upstream.go | 7 +++- templates/swagger/v1_json.tmpl | 4 +++ tests/integration/repo_merge_upstream_test.go | 32 +++++++++++++++++++ 6 files changed, 45 insertions(+), 3 deletions(-) diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index 55c98d60b9..5416f43b0d 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -136,6 +136,7 @@ type UpdateBranchProtectionPriories struct { type MergeUpstreamRequest struct { Branch string `json:"branch"` + FfOnly bool `json:"ff_only"` } type MergeUpstreamResponse struct { diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index fe82550fdd..6e496e6fd3 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -1181,7 +1181,7 @@ func MergeUpstream(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.MergeUpstreamRequest) - mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch) + mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.APIError(http.StatusBadRequest, err) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index dc8a90b2ae..96d1d87836 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -258,7 +258,7 @@ func CreateBranch(ctx *context.Context) { func MergeUpstream(ctx *context.Context) { branchName := ctx.FormString("branch") - _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName) + _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.JSONErrorNotFound() diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go index 34e01df723..8d6f11372c 100644 --- a/services/repository/merge_upstream.go +++ b/services/repository/merge_upstream.go @@ -18,7 +18,7 @@ import ( ) // MergeUpstream merges the base repository's default branch into the fork repository's current branch. -func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { +func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string, ffOnly bool) (mergeStyle string, err error) { if err = repo.MustNotBeArchived(); err != nil { return "", err } @@ -45,6 +45,11 @@ func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_ return "", err } + // If ff_only is requested and fast-forward failed, return error + if ffOnly { + return "", util.NewInvalidArgumentErrorf("fast-forward merge not possible: branch has diverged") + } + // TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment // ideally in the future the "merge" functions should be refactored to decouple from the PullRequest fakeIssue := &issue_model.Issue{ diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 59115431b9..4fa72e16d5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -24839,6 +24839,10 @@ "branch": { "type": "string", "x-go-name": "Branch" + }, + "ff_only": { + "type": "boolean", + "x-go-name": "FfOnly" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go index e928b04e9b..d33d31c646 100644 --- a/tests/integration/repo_merge_upstream_test.go +++ b/tests/integration/repo_merge_upstream_test.go @@ -147,5 +147,37 @@ func TestRepoMergeUpstream(t *testing.T) { return queryMergeUpstreamButtonLink(htmlDoc) == "" }, 5*time.Second, 100*time.Millisecond) }) + + t.Run("FastForwardOnly", func(t *testing.T) { + // Create a clean branch for fast-forward testing + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "new_branch_name": "ff-test-branch", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Add content to base repository that can be fast-forwarded + require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "ff-test.txt", "master", "ff-content-1")) + + // ff_only=true with fast-forward possible (should succeed) + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{ + Branch: "ff-test-branch", + FfOnly: true, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + var mergeResp api.MergeUpstreamResponse + DecodeJSON(t, resp, &mergeResp) + assert.Equal(t, "fast-forward", mergeResp.MergeStyle) + + // ff_only=true when fast-forward is not possible (should fail) + require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "another-file.txt", "master", "more-content")) + + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{ + Branch: "fork-branch", + FfOnly: true, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusBadRequest) + }) }) }