mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-09 00:59:57 +02:00
enhance: fall back to DEFAULT_TEMPLATE.md when style-specific template is missing (#38803)
Closes #38801 Introduce `DEFAULT_TEMPLATE.md` as the default message for all merge styles. https://gitea.com/gitea/docs/pulls/491 By the way, fix incorrect os.Expand usage for merge message & repo template --------- Co-authored-by: waterWang <waterWang@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
co-authored by
waterWang
wxiaoguang
parent
a34cc4cac4
commit
2657756cac
@@ -7,11 +7,18 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
type FastImportInit struct {
|
||||
Bare bool
|
||||
ObjectFormat string
|
||||
}
|
||||
|
||||
type FastImportFile struct {
|
||||
Mode EntryMode
|
||||
Path string
|
||||
@@ -24,6 +31,20 @@ type FastImportCommit struct {
|
||||
Files []FastImportFile
|
||||
}
|
||||
|
||||
// ForceFastImportWithInit is for mainly for testing purpose
|
||||
func ForceFastImportWithInit(ctx context.Context, repoLocalPath string, commits []FastImportCommit, initOpts ...FastImportInit) (RepositoryFacade, error) {
|
||||
repo := gitrepo.RepositoryUnmanaged(repoLocalPath)
|
||||
initOpt := util.OptionalArg(initOpts, FastImportInit{Bare: true})
|
||||
if exist, _ := IsRepositoryExist(ctx, repo); !exist {
|
||||
_ = os.MkdirAll(repoLocalPath, 0o755)
|
||||
err := InitRepositoryLocal(ctx, repoLocalPath, initOpt.Bare, util.IfZero(initOpt.ObjectFormat, "sha1"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return repo, ForceFastImport(ctx, repo, commits)
|
||||
}
|
||||
|
||||
// ForceFastImport is for mainly for testing purpose
|
||||
func ForceFastImport(ctx context.Context, repo RepositoryFacade, commits []FastImportCommit) error {
|
||||
var buf bytes.Buffer
|
||||
|
||||
@@ -149,7 +149,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if hasExtTrackFormat && !ref.IsPull {
|
||||
ctx.RenderOptions.Metas["index"] = ref.Issue
|
||||
|
||||
res, err := vars.Expand(ctx.RenderOptions.Metas["format"], ctx.RenderOptions.Metas)
|
||||
res, err := vars.ExpandCurlyBrace(ctx.RenderOptions.Metas["format"], ctx.RenderOptions.Metas)
|
||||
if err != nil {
|
||||
// here we could just log the error and continue the rendering
|
||||
log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
|
||||
|
||||
@@ -5,14 +5,17 @@ package vars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Expand replaces all variables like {var} by `vars` map, it always returns the expanded string regardless of errors
|
||||
// if error occurs, the error part doesn't change and is returned as it is.
|
||||
func Expand(template string, vars map[string]string) (string, error) {
|
||||
// ExpandCurlyBrace replaces all variables like {var} by `vars` map,
|
||||
// it always returns the expanded string regardless of errors.
|
||||
// if error occurs (wrong syntax, missing variable), the error part doesn't change and is returned as it is.
|
||||
func ExpandCurlyBrace(template string, vars map[string]string) (string, error) {
|
||||
// in the future, if necessary, we can introduce some escape-char,
|
||||
// for example: it will use `#' as a reversed char, templates will use `{#{}` to do escape and output char '{'.
|
||||
var buf strings.Builder
|
||||
@@ -71,3 +74,26 @@ func Expand(template string, vars map[string]string) (string, error) {
|
||||
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
regexpShellLike *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
ret.regexpShellLike = regexp.MustCompile(`(\$\{[a-zA-Z_]\w*\}|\$[a-zA-Z_]\w*)`)
|
||||
return ret
|
||||
})
|
||||
|
||||
// ExpandShellLike works like os.Expand, the difference is that this function keeps the non-existing keys
|
||||
func ExpandShellLike(template string, vars map[string]string) string {
|
||||
re := globalVars().regexpShellLike
|
||||
return re.ReplaceAllStringFunc(template, func(s string) string {
|
||||
key := s[1:]
|
||||
if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") {
|
||||
key = key[1 : len(key)-1]
|
||||
}
|
||||
if val, ok := vars[key]; ok {
|
||||
return val
|
||||
}
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExpandVars(t *testing.T) {
|
||||
kases := []struct {
|
||||
func TestExpandCurlyBrace(t *testing.T) {
|
||||
cases := []struct {
|
||||
tmpl string
|
||||
data map[string]string
|
||||
out string
|
||||
@@ -57,11 +57,11 @@ func TestExpandVars(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, kase := range kases {
|
||||
t.Run(kase.tmpl, func(t *testing.T) {
|
||||
res, err := Expand(kase.tmpl, kase.data)
|
||||
assert.Equal(t, kase.out, res)
|
||||
if kase.error {
|
||||
for _, c := range cases {
|
||||
t.Run(c.tmpl, func(t *testing.T) {
|
||||
res, err := ExpandCurlyBrace(c.tmpl, c.data)
|
||||
assert.Equal(t, c.out, res)
|
||||
if c.error {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
@@ -69,3 +69,19 @@ func TestExpandVars(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandShellLike(t *testing.T) {
|
||||
cases := []struct {
|
||||
tmpl string
|
||||
data map[string]string
|
||||
out string
|
||||
}{
|
||||
{tmpl: "$key ${key} $other ${other}", data: map[string]string{"key": "val"}, out: "val val $other ${other}"},
|
||||
{tmpl: "$ key ${key }", data: map[string]string{"key": "val"}, out: "$ key ${key }"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
out := ExpandShellLike(c.tmpl, c.data)
|
||||
assert.Equal(t, c.out, out, "tmpl: %s", c.tmpl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ func handleViewIssueRedirectExternal(ctx *context.Context) {
|
||||
if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
|
||||
metas := ctx.Repo.Repository.ComposeCommentMetas(ctx)
|
||||
metas["index"] = ctx.PathParam("index")
|
||||
res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
|
||||
res, err := vars.ExpandCurlyBrace(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
|
||||
if err != nil {
|
||||
log.Error("unable to expand template vars for issue url. issue: %s, err: %v", metas["index"], err)
|
||||
ctx.ServerError("Expand", err)
|
||||
|
||||
+26
-9
@@ -32,6 +32,7 @@ import (
|
||||
"gitea.dev/modules/references"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates/vars"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
issue_service "gitea.dev/services/issue"
|
||||
@@ -66,17 +67,15 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
|
||||
reviewedBy := pr.GetApprovers(ctx)
|
||||
|
||||
if mergeStyle != "" {
|
||||
templateFilepath := fmt.Sprintf(".gitea/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle)))
|
||||
commit, err := baseGitRepo.GetBranchCommit(ctx, pr.BaseRepo.DefaultBranch)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
templateContent, err := commit.GetFileContent(ctx, baseGitRepo, templateFilepath, setting.Repository.PullRequest.DefaultMergeMessageSize)
|
||||
templateContent, err := resolveMergeMessageTemplate(ctx, baseGitRepo, commit, mergeStyle)
|
||||
if err != nil {
|
||||
if !git.IsErrNotExist(err) {
|
||||
return "", "", err
|
||||
}
|
||||
} else {
|
||||
return "", "", err
|
||||
}
|
||||
if templateContent != "" {
|
||||
vars := map[string]string{
|
||||
"BaseRepoOwnerName": pr.BaseRepo.OwnerName,
|
||||
"BaseRepoName": pr.BaseRepo.Name,
|
||||
@@ -146,14 +145,32 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
|
||||
return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), body, nil
|
||||
}
|
||||
|
||||
func expandDefaultMergeMessage(template string, vars map[string]string) (message, body string) {
|
||||
// resolveMergeMessageTemplate returns the content of the merge message template for the given
|
||||
// merge style. It first looks for a style-specific template ({STYLE}_TEMPLATE.md), and falls back
|
||||
// to the generic DEFAULT_TEMPLATE.md if the style-specific one is not found.
|
||||
func resolveMergeMessageTemplate(ctx context.Context, baseGitRepo *git.Repository, commit *git.Commit, mergeStyle repo_model.MergeStyle) (string, error) {
|
||||
templateFilepath := fmt.Sprintf(".gitea/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle)))
|
||||
templateContent, err := commit.GetFileContent(ctx, baseGitRepo, templateFilepath, setting.Repository.PullRequest.DefaultMergeMessageSize)
|
||||
if err == nil {
|
||||
return templateContent, nil
|
||||
}
|
||||
if !git.IsErrNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
templateContent, err = commit.GetFileContent(ctx, baseGitRepo, ".gitea/default_merge_message/DEFAULT_TEMPLATE.md", setting.Repository.PullRequest.DefaultMergeMessageSize)
|
||||
if err == nil || git.IsErrNotExist(err) {
|
||||
return templateContent, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func expandDefaultMergeMessage(template string, varsMap map[string]string) (message, body string) {
|
||||
message = strings.TrimSpace(template)
|
||||
if splits := strings.SplitN(message, "\n", 2); len(splits) == 2 {
|
||||
message = splits[0]
|
||||
body = strings.TrimSpace(splits[1])
|
||||
}
|
||||
mapping := func(s string) string { return vars[s] }
|
||||
return os.Expand(message, mapping), os.Expand(body, mapping)
|
||||
return vars.ExpandShellLike(message, varsMap), vars.ExpandShellLike(body, varsMap)
|
||||
}
|
||||
|
||||
// GetDefaultMergeMessage returns default message used when merging pull request
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
package pull
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/git"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_expandDefaultMergeMessage(t *testing.T) {
|
||||
@@ -90,3 +94,47 @@ func TestAddCommitMessageTailer(t *testing.T) {
|
||||
assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTailer("title\n\nTest-tailer: v1", "Test-tailer", "v2"))
|
||||
assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTailer("title\n\nTest-tailer: v1\n", "Test-tailer", "v2"))
|
||||
}
|
||||
|
||||
func TestResolveMergeMessageTemplate(t *testing.T) {
|
||||
t.Run("NoDefault", func(t *testing.T) {
|
||||
repo, err := git.ForceFastImportWithInit(t.Context(), filepath.Join(t.TempDir(), "test-repo"), []git.FastImportCommit{
|
||||
{Ref: "refs/heads/master", Files: []git.FastImportFile{
|
||||
{Path: ".gitea/default_merge_message/REBASE_TEMPLATE.md", Content: "rebase template"},
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
gitRepo, err := git.OpenRepository(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetBranchCommit(t.Context(), "master")
|
||||
require.NoError(t, err)
|
||||
tmpl, err := resolveMergeMessageTemplate(t.Context(), gitRepo, commit, "merge")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", tmpl)
|
||||
tmpl, err = resolveMergeMessageTemplate(t.Context(), gitRepo, commit, "rebase")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "rebase template", tmpl)
|
||||
})
|
||||
t.Run("WithDefault", func(t *testing.T) {
|
||||
repo, err := git.ForceFastImportWithInit(t.Context(), filepath.Join(t.TempDir(), "test-repo"), []git.FastImportCommit{
|
||||
{Ref: "refs/heads/master", Files: []git.FastImportFile{
|
||||
{Path: ".gitea/default_merge_message/DEFAULT_TEMPLATE.md", Content: "default template"},
|
||||
{Path: ".gitea/default_merge_message/REBASE_TEMPLATE.md", Content: "rebase template"},
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
gitRepo, err := git.OpenRepository(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetBranchCommit(t.Context(), "master")
|
||||
require.NoError(t, err)
|
||||
tmpl, err := resolveMergeMessageTemplate(t.Context(), gitRepo, commit, "merge")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "default template", tmpl)
|
||||
tmpl, err = resolveMergeMessageTemplate(t.Context(), gitRepo, commit, "rebase")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "rebase template", tmpl)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir
|
||||
"CloneURL.HTTPS": cloneLink.HTTPS,
|
||||
"OwnerName": repo.OwnerName,
|
||||
}
|
||||
res, err := vars.Expand(string(data), match)
|
||||
res, err := vars.ExpandCurlyBrace(string(data), match)
|
||||
if err != nil {
|
||||
// here we could just log the error and continue the rendering
|
||||
log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err)
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates/vars"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/huandu/xstrings"
|
||||
@@ -86,12 +87,7 @@ func generateExpansion(ctx context.Context, src string, templateRepo, generateRe
|
||||
}
|
||||
}
|
||||
|
||||
return os.Expand(src, func(key string) string {
|
||||
if val, ok := expansionMap[key]; ok {
|
||||
return val
|
||||
}
|
||||
return key
|
||||
})
|
||||
return vars.ExpandShellLike(src, expansionMap)
|
||||
}
|
||||
|
||||
type giteaTemplateFileMatcher struct {
|
||||
|
||||
Reference in New Issue
Block a user