feat: Retrieve default merge commit message for pull requests (#10022)

Closes #7719

Co-authored-by: kukolos <91948790+kukolos@users.noreply.github.com>
Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10022
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: MayMeow <maymeow@noreply.codeberg.org>
Co-committed-by: MayMeow <maymeow@noreply.codeberg.org>
This commit is contained in:
MayMeow 2026-01-01 11:45:43 +01:00 committed by 0ko
commit 2566924ff8
4 changed files with 118 additions and 1 deletions

View file

@ -240,6 +240,11 @@
},
"teams.add_all_repos.modal.header": "Add all repositories",
"teams.remove_all_repos.modal.header": "Remove all repositories",
"pulls.manual_merge.helper": "Manual merge helper",
"pulls.manual_merge.helpder.description": "Use this merge commit message when completing the merge manually.",
"pulls.manual_merge.commit.title": "Merge commit title",
"pulls.manual_merge.commit.body": "Merge commit body",
"pulls.manual_merge.copy.button": "Copy merge commit message",
"admin.auths.oauth2_quota_group_claim_name": "Claim name providing group names for this source to be used for quota management. (Optional)",
"admin.auths.oauth2_quota_group_map": "Map claimed groups to quota groups. (Optional - requires claim name above)",
"admin.auths.oauth2_quota_group_map_removal": "Remove users from synchronized quota groups if user does not belong to corresponding group.",

View file

@ -390,7 +390,7 @@
{{end}}
{{if and .Issue.PullRequest.HeadRepo (not .Issue.PullRequest.HasMerged) (not .Issue.IsClosed)}}
{{template "repo/issue/view_content/pull_merge_instruction" dict "PullRequest" .Issue.PullRequest "ShowMergeInstructions" .ShowMergeInstructions "AutodetectManualMerge" .AutodetectManualMerge}}
{{template "repo/issue/view_content/pull_merge_instruction" dict "PullRequest" .Issue.PullRequest "ShowMergeInstructions" .ShowMergeInstructions "AutodetectManualMerge" .AutodetectManualMerge "DefaultMergeMessage" .DefaultMergeMessage "DefaultMergeBody" .DefaultMergeBody "IsPullFilesConflicted" .IsPullFilesConflicted}}
{{end}}
</div>
</div>

View file

@ -1,6 +1,35 @@
<div class="divider"></div>
<details class="collapsible">
<summary class="tw-py-2"> {{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint"}} </summary>
{{if .IsPullFilesConflicted}}
{{$titleID := printf "manual-merge-title-%d" .PullRequest.ID}}
{{$bodyID := printf "manual-merge-body-%d" .PullRequest.ID}}
{{$copySourceID := printf "manual-merge-message-%d" .PullRequest.ID}}
{{$fullMessage := .DefaultMergeMessage}}
{{if .DefaultMergeBody}}
{{$fullMessage = printf "%s\n\n%s" .DefaultMergeMessage .DefaultMergeBody}}
{{end}}
<div class="tw-mb-4">
<h3 class="tw-flex tw-items-center tw-gap-2">{{svg "octicon-info" 16}} {{ctx.Locale.Tr "pulls.manual_merge.helper"}}</h3>
<p class="tw-mb-2">{{ctx.Locale.Tr "pulls.manual_merge.helpder.description"}}</p>
<div class="ui secondary segment tw-space-y-3">
<div class="ui form">
<div class="field">
<label for="{{$titleID}}">{{ctx.Locale.Tr "pulls.manual_merge.commit.title"}}</label>
<textarea id="{{$titleID}}" readonly rows="1">{{- .DefaultMergeMessage -}}</textarea>
</div>
{{if .DefaultMergeBody}}
<div class="field">
<label for="{{$bodyID}}">{{ctx.Locale.Tr "pulls.manual_merge.commit.body"}}</label>
<textarea id="{{$bodyID}}" readonly rows="5">{{- .DefaultMergeBody -}}</textarea>
</div>
{{end}}
<textarea id="{{$copySourceID}}" class="tw-hidden" aria-hidden="true" readonly>{{- $fullMessage -}}</textarea>
<button class="secondary button" type="button" data-clipboard-target="#{{$copySourceID}}">{{svg "octicon-copy" 16}} {{ctx.Locale.Tr "pulls.manual_merge.copy.button"}}</button>
</div>
</div>
</div>
{{end}}
<div><h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}</div>
{{$localBranch := .PullRequest.HeadBranch}}
{{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}

View file

@ -0,0 +1,83 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
auth_model "forgejo.org/models/auth"
issues_model "forgejo.org/models/issues"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
"forgejo.org/modules/gitrepo"
api "forgejo.org/modules/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPullMergeInstruction(t *testing.T) {
onApplicationRun(t, func(t *testing.T, _ *url.URL) {
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
// Use API to create a conflicting PR, mirroring TestCantMergeConflict
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
Head: "conflict",
Base: "base",
Title: "create a conflicting pr",
}).AddTokenAuth(token)
resp := session.MakeRequest(t, req, http.StatusCreated)
var pr api.PullRequest
DecodeJSON(t, resp, &pr)
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
Name: "user1",
})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
OwnerID: user1.ID,
Name: "repo1",
})
// Assert that the PR exists, is open, and is not mergeable (conflicted)
prLoaded := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
ID: pr.ID,
HeadRepoID: repo1.ID,
BaseRepoID: repo1.ID,
HeadBranch: "conflict",
BaseBranch: "base",
}, "status = 0")
assert.False(t, prLoaded.Mergeable(t.Context()), "PR should be marked as conflicted")
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo1)
require.NoError(t, err)
defer gitRepo.Close()
// Visit the PR page and check for the manual merge helper
req = NewRequest(t, "GET", fmt.Sprintf("/user1/repo1/pulls/%d", pr.Index))
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
// Check for "View command line instructions"
summary := htmlDoc.doc.Find("details.collapsible summary").Text()
assert.Contains(t, summary, "View command line instructions")
// Check for "Manual merge helper"
helperTitle := htmlDoc.doc.Find("details.collapsible h3").Text()
assert.Contains(t, helperTitle, "Manual merge helper")
// Check for the description
helperDesc := htmlDoc.doc.Find("details.collapsible p").Text()
assert.Contains(t, helperDesc, "Use this merge commit message when completing the merge manually.")
})
}