mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-21 14:34:43 +02:00
Merge branch 'main' into workflow-email
This commit is contained in:
commit
9da6b36646
@ -37,6 +37,14 @@ type PackageVersion struct {
|
||||
DownloadCount int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
// IsPrerelease checks if the version is a prerelease version according to semantic versioning
|
||||
func (pv *PackageVersion) IsPrerelease() bool {
|
||||
if pv == nil || pv.Version == "" {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(pv.Version, "-")
|
||||
}
|
||||
|
||||
// GetOrInsertVersion inserts a version. If the same version exist already ErrDuplicatePackageVersion is returned
|
||||
func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) {
|
||||
e := db.GetEngine(ctx)
|
||||
|
@ -5,12 +5,14 @@ package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// AutoMerge represents a pull request scheduled for merging when checks succeed
|
||||
@ -76,7 +78,10 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
|
||||
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
doer, err = user_model.NewGhostUser(), nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ type Metadata struct {
|
||||
ReleaseNotes string `json:"release_notes,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
RequireLicenseAcceptance bool `json:"require_license_acceptance"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
|
||||
@ -105,6 +106,7 @@ type nuspecPackage struct {
|
||||
Readme string `xml:"readme"`
|
||||
ReleaseNotes string `xml:"releaseNotes"`
|
||||
RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"`
|
||||
Summary string `xml:"summary"`
|
||||
Tags string `xml:"tags"`
|
||||
Title string `xml:"title"`
|
||||
|
||||
@ -204,6 +206,7 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
|
||||
ReleaseNotes: p.Metadata.ReleaseNotes,
|
||||
RepositoryURL: p.Metadata.Repository.URL,
|
||||
RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
|
||||
Summary: p.Metadata.Summary,
|
||||
Tags: p.Metadata.Tags,
|
||||
Title: p.Metadata.Title,
|
||||
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
texttmpl "text/template"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -16,6 +17,12 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type MailTemplates struct {
|
||||
TemplateNames []string
|
||||
BodyTemplates *template.Template
|
||||
SubjectTemplates *texttmpl.Template
|
||||
}
|
||||
|
||||
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
||||
|
||||
// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
|
||||
@ -52,16 +59,17 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mailer provides the templates required for sending notification mails.
|
||||
func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
// LoadMailTemplates provides the templates required for sending notification mails.
|
||||
func LoadMailTemplates(ctx context.Context, loadedTemplates *atomic.Pointer[MailTemplates]) {
|
||||
assetFS := AssetFS()
|
||||
refreshTemplates := func(firstRun bool) {
|
||||
var templateNames []string
|
||||
subjectTemplates := texttmpl.New("")
|
||||
bodyTemplates := template.New("")
|
||||
|
||||
subjectTemplates.Funcs(mailSubjectTextFuncMap())
|
||||
bodyTemplates.Funcs(NewFuncMap())
|
||||
|
||||
assetFS := AssetFS()
|
||||
refreshTemplates := func(firstRun bool) {
|
||||
if !firstRun {
|
||||
log.Trace("Reloading mail templates")
|
||||
}
|
||||
@ -81,6 +89,7 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
if firstRun {
|
||||
log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
|
||||
}
|
||||
templateNames = append(templateNames, tmplName)
|
||||
if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
|
||||
if firstRun {
|
||||
log.Fatal("Failed to parse mail template, err: %v", err)
|
||||
@ -88,6 +97,12 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
log.Error("Failed to parse mail template, err: %v", err)
|
||||
}
|
||||
}
|
||||
loaded := &MailTemplates{
|
||||
TemplateNames: templateNames,
|
||||
BodyTemplates: bodyTemplates,
|
||||
SubjectTemplates: subjectTemplates,
|
||||
}
|
||||
loadedTemplates.Store(loaded)
|
||||
}
|
||||
|
||||
refreshTemplates(true)
|
||||
@ -99,6 +114,4 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
refreshTemplates(false)
|
||||
})
|
||||
}
|
||||
|
||||
return subjectTemplates, bodyTemplates
|
||||
}
|
||||
|
@ -2334,6 +2334,8 @@ settings.hooks_desc=Déanann Crúcaí Gréasán iarratais HTTP POST go huathoibr
|
||||
settings.webhook_deletion=Bain Crúca Gréasán
|
||||
settings.webhook_deletion_desc=Scriostar a shocruithe agus a stair seachadta a bhaineann le Crúca Gréasán a bhaint. Lean ar aghaidh?
|
||||
settings.webhook_deletion_success=Tá an Crúca Gréasán bainte amach.
|
||||
settings.webhook.test_delivery=Imeacht Brúigh Tástála
|
||||
settings.webhook.test_delivery_desc=Déan tástáil ar an webhook seo le teagmhas brú bréige.
|
||||
settings.webhook.test_delivery_desc_disabled=Chun an Crúca Gréasán seo a thástáil le himeacht bhréige, gníomhachtaigh é.
|
||||
settings.webhook.request=Iarratas
|
||||
settings.webhook.response=Freagra
|
||||
@ -2353,6 +2355,7 @@ settings.payload_url=URL spriocdhírithe
|
||||
settings.http_method=Modh HTTP
|
||||
settings.content_type=Cineál Ábhar POST
|
||||
settings.secret=Rúnda
|
||||
settings.webhook_secret_desc=Más féidir le freastalaí an webhook rún a úsáid, is féidir leat lámhleabhar an webhook a leanúint agus rún a líonadh isteach anseo.
|
||||
settings.slack_username=Ainm úsáideora
|
||||
settings.slack_icon_url=URL deilbhín
|
||||
settings.slack_color=Dath
|
||||
|
@ -2353,6 +2353,7 @@ settings.payload_url=URL de destino
|
||||
settings.http_method=Método HTTP
|
||||
settings.content_type=Tipo de conteúdo POST
|
||||
settings.secret=Segredo
|
||||
settings.webhook_secret_desc=Se o servidor de automatismos web suportar a utilização de segredos, você pode seguir o manual do automatismo web e preencher um segredo aqui.
|
||||
settings.slack_username=Nome de utilizador
|
||||
settings.slack_icon_url=URL do ícone
|
||||
settings.slack_color=Cor
|
||||
|
@ -1400,12 +1400,12 @@ editor.revert=将 %s 还原到:
|
||||
editor.failed_to_commit=提交更改失败。
|
||||
editor.failed_to_commit_summary=错误信息:
|
||||
|
||||
editor.fork_create=派生仓库发起请求变更
|
||||
editor.fork_create_description=您不能直接编辑此仓库。您可以从此仓库派生,进行编辑并创建一个拉取请求。
|
||||
editor.fork_edit_description=您不能直接编辑此仓库。 更改将写入您的派生仓库 <b>%s</b>,以便您可以创建一个拉取请求。
|
||||
editor.fork_not_editable=你已经派生了这个仓库,但是你的分叉是不可编辑的。
|
||||
editor.fork_create=派生仓库以请求变更
|
||||
editor.fork_create_description=您不能直接编辑此仓库。您可以派生此仓库,进行编辑并创建一个合并请求。
|
||||
editor.fork_edit_description=您不能直接编辑此仓库。 更改将写入您的派生仓库 <b>%s</b>,以便您可以创建一个合并请求。
|
||||
editor.fork_not_editable=您已经派生了此仓库,但您的派生是不可编辑的。
|
||||
editor.fork_failed_to_push_branch=推送分支 %s 到仓库失败。
|
||||
editor.fork_branch_exists=分支 "%s" 已存在于您的派生仓库中,请选择一个新的分支名称。
|
||||
editor.fork_branch_exists=分支「%s」已存在于您的派生仓库中,请选择一个新的分支名称。
|
||||
|
||||
commits.desc=浏览代码修改历史
|
||||
commits.commits=次代码提交
|
||||
@ -2171,8 +2171,8 @@ settings.hooks=Web 钩子
|
||||
settings.githooks=管理 Git 钩子
|
||||
settings.basic_settings=基本设置
|
||||
settings.mirror_settings=镜像设置
|
||||
settings.mirror_settings.docs=设置您的仓库以自动同步另一个仓库的提交、标签和分支。
|
||||
settings.mirror_settings.docs.disabled_pull_mirror.instructions=设置您的项目以自动将提交、标签和分支推送到另一个仓库。您的站点管理员已禁用了拉取镜像。
|
||||
settings.mirror_settings.docs=将您的仓库设置为自动同步另一个仓库的提交、标签和分支。
|
||||
settings.mirror_settings.docs.disabled_pull_mirror.instructions=将您的项目设置为自动将提交、标签和分支推送到另一个仓库。您的站点管理员已禁用了拉取镜像。
|
||||
settings.mirror_settings.docs.disabled_push_mirror.instructions=将您的项目设置为自动从一个仓库拉取提交、标签和分支。
|
||||
settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=现在,这只能在「迁移外部仓库」菜单中完成。欲了解更多信息,请参考:
|
||||
settings.mirror_settings.docs.disabled_push_mirror.info=您的站点管理员已禁用推送镜像。
|
||||
@ -2335,6 +2335,8 @@ settings.hooks_desc=当 Gitea 事件发生时,Web 钩子自动发出 HTTP POST
|
||||
settings.webhook_deletion=删除 Web 钩子
|
||||
settings.webhook_deletion_desc=删除 Web 钩子将删除其设置和历史记录。继续?
|
||||
settings.webhook_deletion_success=Web 钩子删除成功!
|
||||
settings.webhook.test_delivery=测试推送事件
|
||||
settings.webhook.test_delivery_desc=用假推送事件测试这个 Web 钩子。
|
||||
settings.webhook.test_delivery_desc_disabled=要用假事件测试这个 Web钩子,请激活它。
|
||||
settings.webhook.request=请求内容
|
||||
settings.webhook.response=响应内容
|
||||
@ -2354,6 +2356,7 @@ settings.payload_url=目标 URL
|
||||
settings.http_method=HTTP 方法
|
||||
settings.content_type=POST 内容类型
|
||||
settings.secret=密钥
|
||||
settings.webhook_secret_desc=如果 Webhook 服务器支持使用密钥,您可以按照 Webhook 的手册在此处填写一个密钥。
|
||||
settings.slack_username=服务名称
|
||||
settings.slack_icon_url=图标 URL
|
||||
settings.slack_color=颜色
|
||||
@ -2768,6 +2771,8 @@ branch.new_branch_from=基于「%s」创建新分支
|
||||
branch.renamed=分支 %s 已重命名为 %s。
|
||||
branch.rename_default_or_protected_branch_error=只有管理员能重命名默认分支和受保护的分支。
|
||||
branch.rename_protected_branch_failed=此分支受到 glob 语法规则的保护。
|
||||
branch.commits_divergence_from=提交分歧:落后 %[3]s %[1]d 个提交,领先 %[2]d 个提交
|
||||
branch.commits_no_divergence=与分支 %[1]s 相同
|
||||
|
||||
tag.create_tag=创建标签 %s
|
||||
tag.create_tag_operation=创建标签
|
||||
@ -2781,6 +2786,7 @@ topic.done=保存
|
||||
topic.count_prompt=您最多选择25个主题
|
||||
topic.format_prompt=主题必须以字母或数字开头,可以包含连字符 ('-') 和句点 ('.'),长度不得超过35个字符。字符必须为小写。
|
||||
|
||||
find_file.follow_symlink=跟随此符号链接的指向位置
|
||||
find_file.go_to_file=转到文件
|
||||
find_file.no_matching=没有找到匹配的文件
|
||||
|
||||
|
@ -53,15 +53,23 @@ type RegistrationIndexPageItem struct {
|
||||
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry
|
||||
type CatalogEntry struct {
|
||||
CatalogLeafURL string `json:"@id"`
|
||||
PackageContentURL string `json:"packageContent"`
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
ReleaseNotes string `json:"releaseNotes"`
|
||||
Authors string `json:"authors"`
|
||||
RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
|
||||
ProjectURL string `json:"projectURL"`
|
||||
Copyright string `json:"copyright"`
|
||||
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
|
||||
Description string `json:"description"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
ID string `json:"id"`
|
||||
IsPrerelease bool `json:"isPrerelease"`
|
||||
Language string `json:"language"`
|
||||
LicenseURL string `json:"licenseUrl"`
|
||||
PackageContentURL string `json:"packageContent"`
|
||||
ProjectURL string `json:"projectUrl"`
|
||||
RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
|
||||
Summary string `json:"summary"`
|
||||
Tags string `json:"tags"`
|
||||
Version string `json:"version"`
|
||||
ReleaseNotes string `json:"releaseNotes"`
|
||||
Published time.Time `json:"published"`
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group
|
||||
@ -110,14 +118,23 @@ func createRegistrationIndexPageItem(l *linkBuilder, pd *packages_model.PackageD
|
||||
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
|
||||
CatalogEntry: &CatalogEntry{
|
||||
CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
|
||||
ID: pd.Package.Name,
|
||||
Version: pd.Version.Version,
|
||||
Description: metadata.Description,
|
||||
ReleaseNotes: metadata.ReleaseNotes,
|
||||
Authors: metadata.Authors,
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
Copyright: metadata.Copyright,
|
||||
DependencyGroups: createDependencyGroups(pd),
|
||||
Description: metadata.Description,
|
||||
IconURL: metadata.IconURL,
|
||||
ID: pd.Package.Name,
|
||||
IsPrerelease: pd.Version.IsPrerelease(),
|
||||
Language: metadata.Language,
|
||||
LicenseURL: metadata.LicenseURL,
|
||||
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
|
||||
Summary: metadata.Summary,
|
||||
Tags: metadata.Tags,
|
||||
Version: pd.Version.Version,
|
||||
ReleaseNotes: metadata.ReleaseNotes,
|
||||
Published: pd.Version.CreatedUnix.AsLocalTime(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -147,20 +164,40 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe
|
||||
type RegistrationLeafResponse struct {
|
||||
RegistrationLeafURL string `json:"@id"`
|
||||
Type []string `json:"@type"`
|
||||
Listed bool `json:"listed"`
|
||||
PackageContentURL string `json:"packageContent"`
|
||||
Published time.Time `json:"published"`
|
||||
RegistrationIndexURL string `json:"registration"`
|
||||
CatalogEntry CatalogEntry `json:"catalogEntry"`
|
||||
}
|
||||
|
||||
func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationLeafResponse {
|
||||
registrationLeafURL := l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version)
|
||||
packageDownloadURL := l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version)
|
||||
metadata := pd.Metadata.(*nuget_module.Metadata)
|
||||
return &RegistrationLeafResponse{
|
||||
Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
|
||||
Listed: true,
|
||||
Published: pd.Version.CreatedUnix.AsLocalTime(),
|
||||
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
|
||||
RegistrationLeafURL: registrationLeafURL,
|
||||
RegistrationIndexURL: l.GetRegistrationIndexURL(pd.Package.Name),
|
||||
PackageContentURL: packageDownloadURL,
|
||||
Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
|
||||
CatalogEntry: CatalogEntry{
|
||||
CatalogLeafURL: registrationLeafURL,
|
||||
Authors: metadata.Authors,
|
||||
Copyright: metadata.Copyright,
|
||||
DependencyGroups: createDependencyGroups(pd),
|
||||
Description: metadata.Description,
|
||||
IconURL: metadata.IconURL,
|
||||
ID: pd.Package.Name,
|
||||
IsPrerelease: pd.Version.IsPrerelease(),
|
||||
Language: metadata.Language,
|
||||
LicenseURL: metadata.LicenseURL,
|
||||
PackageContentURL: packageDownloadURL,
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
|
||||
Summary: metadata.Summary,
|
||||
Tags: metadata.Tags,
|
||||
Version: pd.Version.Version,
|
||||
ReleaseNotes: metadata.ReleaseNotes,
|
||||
Published: pd.Version.CreatedUnix.AsLocalTime(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,12 +225,23 @@ type SearchResultResponse struct {
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
|
||||
type SearchResult struct {
|
||||
Authors string `json:"authors"`
|
||||
Copyright string `json:"copyright"`
|
||||
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
|
||||
Description string `json:"description"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
ID string `json:"id"`
|
||||
IsPrerelease bool `json:"isPrerelease"`
|
||||
Language string `json:"language"`
|
||||
LicenseURL string `json:"licenseUrl"`
|
||||
ProjectURL string `json:"projectUrl"`
|
||||
RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
|
||||
Summary string `json:"summary"`
|
||||
Tags string `json:"tags"`
|
||||
Title string `json:"title"`
|
||||
TotalDownloads int64 `json:"totalDownloads"`
|
||||
Version string `json:"version"`
|
||||
Versions []*SearchResultVersion `json:"versions"`
|
||||
Description string `json:"description"`
|
||||
Authors string `json:"authors"`
|
||||
ProjectURL string `json:"projectURL"`
|
||||
RegistrationIndexURL string `json:"registration"`
|
||||
}
|
||||
|
||||
@ -230,11 +278,12 @@ func createSearchResultResponse(l *linkBuilder, totalHits int64, pds []*packages
|
||||
func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) *SearchResult {
|
||||
latest := pds[0]
|
||||
versions := make([]*SearchResultVersion, 0, len(pds))
|
||||
totalDownloads := int64(0)
|
||||
for _, pd := range pds {
|
||||
if latest.SemVer.LessThan(pd.SemVer) {
|
||||
latest = pd
|
||||
}
|
||||
|
||||
totalDownloads += pd.Version.DownloadCount
|
||||
versions = append(versions, &SearchResultVersion{
|
||||
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
Version: pd.Version.Version,
|
||||
@ -244,12 +293,23 @@ func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor)
|
||||
metadata := latest.Metadata.(*nuget_module.Metadata)
|
||||
|
||||
return &SearchResult{
|
||||
Authors: metadata.Authors,
|
||||
Copyright: metadata.Copyright,
|
||||
Description: metadata.Description,
|
||||
DependencyGroups: createDependencyGroups(latest),
|
||||
IconURL: metadata.IconURL,
|
||||
ID: latest.Package.Name,
|
||||
IsPrerelease: latest.Version.IsPrerelease(),
|
||||
Language: metadata.Language,
|
||||
LicenseURL: metadata.LicenseURL,
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
|
||||
Summary: metadata.Summary,
|
||||
Tags: metadata.Tags,
|
||||
Title: metadata.Title,
|
||||
TotalDownloads: totalDownloads,
|
||||
Version: latest.Version.Version,
|
||||
Versions: versions,
|
||||
Description: metadata.Description,
|
||||
Authors: metadata.Authors,
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
|
||||
}
|
||||
}
|
||||
|
58
routers/web/devtest/mail_preview.go
Normal file
58
routers/web/devtest/mail_preview.go
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package devtest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func MailPreviewRender(ctx *context.Context) {
|
||||
tmplName := ctx.PathParam("*")
|
||||
mockDataContent, err := templates.AssetFS().ReadFile("mail/" + tmplName + ".mock.yml")
|
||||
mockData := map[string]any{}
|
||||
if err == nil {
|
||||
err = yaml.Unmarshal(mockDataContent, &mockData)
|
||||
if err != nil {
|
||||
http.Error(ctx.Resp, "Failed to parse mock data: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
mockData["locale"] = ctx.Locale
|
||||
err = mailer.LoadedTemplates().BodyTemplates.ExecuteTemplate(ctx.Resp, tmplName, mockData)
|
||||
if err != nil {
|
||||
_, _ = ctx.Resp.Write([]byte(err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMailPreviewRender(ctx *context.Context, tmplName string) {
|
||||
tmplSubject := mailer.LoadedTemplates().SubjectTemplates.Lookup(tmplName)
|
||||
if tmplSubject == nil {
|
||||
ctx.Data["RenderMailSubject"] = "default subject"
|
||||
} else {
|
||||
var buf strings.Builder
|
||||
err := tmplSubject.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
ctx.Data["RenderMailSubject"] = err.Error()
|
||||
} else {
|
||||
ctx.Data["RenderMailSubject"] = buf.String()
|
||||
}
|
||||
}
|
||||
ctx.Data["RenderMailTemplateName"] = tmplName
|
||||
}
|
||||
|
||||
func MailPreview(ctx *context.Context) {
|
||||
ctx.Data["MailTemplateNames"] = mailer.LoadedTemplates().TemplateNames
|
||||
tmplName := ctx.FormString("tmpl")
|
||||
if tmplName != "" {
|
||||
prepareMailPreviewRender(ctx, tmplName)
|
||||
}
|
||||
ctx.HTML(http.StatusOK, "devtest/mail-preview")
|
||||
}
|
@ -1659,6 +1659,8 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Group("/devtest", func() {
|
||||
m.Any("", devtest.List)
|
||||
m.Any("/fetch-action-test", devtest.FetchActionTest)
|
||||
m.Any("/mail-preview", devtest.MailPreview)
|
||||
m.Any("/mail-preview/*", devtest.MailPreviewRender)
|
||||
m.Any("/{sub}", devtest.TmplCommon)
|
||||
m.Get("/repo-action-view/{run}/{job}", devtest.MockActionsView)
|
||||
m.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs)
|
||||
|
@ -22,23 +22,21 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
// prAutoMergeQueue represents a queue to handle update pull request tests
|
||||
var prAutoMergeQueue *queue.WorkerPoolQueue[string]
|
||||
|
||||
// Init runs the task queue to that handles auto merges
|
||||
func Init() error {
|
||||
notify_service.RegisterNotifier(NewNotifier())
|
||||
|
||||
prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
|
||||
if prAutoMergeQueue == nil {
|
||||
automergequeue.AutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
|
||||
if automergequeue.AutoMergeQueue == nil {
|
||||
return errors.New("unable to create pr_auto_merge queue")
|
||||
}
|
||||
go graceful.GetManager().RunWithCancel(prAutoMergeQueue)
|
||||
go graceful.GetManager().RunWithCancel(automergequeue.AutoMergeQueue)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -56,24 +54,23 @@ func handler(items ...string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func addToQueue(pr *issues_model.PullRequest, sha string) {
|
||||
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
|
||||
if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
|
||||
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
|
||||
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) (scheduled bool, err error) {
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message, deleteBranchAfterMerge); err != nil {
|
||||
return err
|
||||
}
|
||||
scheduled = true
|
||||
|
||||
_, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
|
||||
return err
|
||||
})
|
||||
// Old code made "scheduled" to be true after "ScheduleAutoMerge", but it's not right:
|
||||
// If the transaction rolls back, then the pull request is not scheduled to auto merge.
|
||||
// So we should only set "scheduled" to true if there is no error.
|
||||
scheduled = err == nil
|
||||
if scheduled {
|
||||
log.Trace("Pull request [%d] scheduled for auto merge with style [%s] and message [%s]", pull.ID, style, message)
|
||||
automergequeue.StartPRCheckAndAutoMerge(ctx, pull)
|
||||
}
|
||||
return scheduled, err
|
||||
}
|
||||
|
||||
@ -99,38 +96,12 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m
|
||||
}
|
||||
|
||||
for _, pr := range pulls {
|
||||
addToQueue(pr, sha)
|
||||
automergequeue.AddToQueue(pr, sha)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
|
||||
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
|
||||
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||
if err != nil {
|
||||
log.Error("GetRefCommitID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
addToQueue(pull, commitID)
|
||||
}
|
||||
|
||||
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
@ -45,7 +46,7 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo
|
||||
return
|
||||
}
|
||||
// as reviews could have blocked a pending automerge let's recheck
|
||||
StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
|
||||
automergequeue.StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
|
||||
}
|
||||
|
||||
func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
|
||||
|
49
services/automergequeue/automergequeue.go
Normal file
49
services/automergequeue/automergequeue.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package automergequeue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
)
|
||||
|
||||
var AutoMergeQueue *queue.WorkerPoolQueue[string]
|
||||
|
||||
var AddToQueue = func(pr *issues_model.PullRequest, sha string) {
|
||||
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
|
||||
if err := AutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
|
||||
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
|
||||
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
|
||||
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||
if err != nil {
|
||||
log.Error("GetRefCommitID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
AddToQueue(pull, commitID)
|
||||
}
|
@ -15,7 +15,7 @@ import (
|
||||
"mime"
|
||||
"regexp"
|
||||
"strings"
|
||||
texttmpl "text/template"
|
||||
"sync/atomic"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
sender_service "code.gitea.io/gitea/services/mailer/sender"
|
||||
|
||||
@ -31,11 +32,13 @@ import (
|
||||
|
||||
const mailMaxSubjectRunes = 256 // There's no actual limit for subject in RFC 5322
|
||||
|
||||
var (
|
||||
bodyTemplates *template.Template
|
||||
subjectTemplates *texttmpl.Template
|
||||
subjectRemoveSpaces = regexp.MustCompile(`[\s]+`)
|
||||
)
|
||||
var loadedTemplates atomic.Pointer[templates.MailTemplates]
|
||||
|
||||
var subjectRemoveSpaces = regexp.MustCompile(`[\s]+`)
|
||||
|
||||
func LoadedTemplates() *templates.MailTemplates {
|
||||
return loadedTemplates.Load()
|
||||
}
|
||||
|
||||
// SendTestMail sends a test mail
|
||||
func SendTestMail(email string) error {
|
||||
|
@ -120,7 +120,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
||||
}
|
||||
|
||||
var mailSubject bytes.Buffer
|
||||
if err := subjectTemplates.ExecuteTemplate(&mailSubject, tplName, mailMeta); err == nil {
|
||||
if err := LoadedTemplates().SubjectTemplates.ExecuteTemplate(&mailSubject, tplName, mailMeta); err == nil {
|
||||
subject = sanitizeSubject(mailSubject.String())
|
||||
if subject == "" {
|
||||
subject = fallback
|
||||
@ -135,7 +135,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
||||
|
||||
var mailBody bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&mailBody, tplName, mailMeta); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&mailBody, tplName, mailMeta); err != nil {
|
||||
log.Error("ExecuteTemplate [%s]: %v", tplName+"/body", err)
|
||||
}
|
||||
|
||||
@ -261,14 +261,14 @@ func actionToTemplate(issue *issues_model.Issue, actionType activities_model.Act
|
||||
}
|
||||
|
||||
template = typeName + "/" + name
|
||||
ok := bodyTemplates.Lookup(template) != nil
|
||||
ok := LoadedTemplates().BodyTemplates.Lookup(template) != nil
|
||||
if !ok && typeName != "issue" {
|
||||
template = "issue/" + name
|
||||
ok = bodyTemplates.Lookup(template) != nil
|
||||
ok = LoadedTemplates().BodyTemplates.Lookup(template) != nil
|
||||
}
|
||||
if !ok {
|
||||
template = typeName + "/default"
|
||||
ok = bodyTemplates.Lookup(template) != nil
|
||||
ok = LoadedTemplates().BodyTemplates.Lookup(template) != nil
|
||||
}
|
||||
if !ok {
|
||||
template = "issue/default"
|
||||
|
@ -79,7 +79,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re
|
||||
|
||||
var mailBody bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil {
|
||||
log.Error("ExecuteTemplate [%s]: %v", string(tplNewReleaseMail)+"/body", err)
|
||||
return
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
|
||||
"Destination": destination,
|
||||
}
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailRepoTransferNotify), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailRepoTransferNotify), data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod
|
||||
}
|
||||
|
||||
var mailBody bytes.Buffer
|
||||
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplTeamInviteMail), mailMeta); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&mailBody, string(tplTeamInviteMail), mailMeta); err != nil {
|
||||
log.Error("ExecuteTemplate [%s]: %v", string(tplTeamInviteMail)+"/body", err)
|
||||
return err
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/attachment"
|
||||
sender_service "code.gitea.io/gitea/services/mailer/sender"
|
||||
@ -96,6 +97,13 @@ func prepareMailerBase64Test(t *testing.T) (doer *user_model.User, repo *repo_mo
|
||||
return user, repo, issue, att1, att2
|
||||
}
|
||||
|
||||
func prepareMailTemplates(name, subjectTmpl, bodyTmpl string) {
|
||||
loadedTemplates.Store(&templates.MailTemplates{
|
||||
SubjectTemplates: texttmpl.Must(texttmpl.New(name).Parse(subjectTmpl)),
|
||||
BodyTemplates: template.Must(template.New(name).Parse(bodyTmpl)),
|
||||
})
|
||||
}
|
||||
|
||||
func TestComposeIssueComment(t *testing.T) {
|
||||
doer, _, issue, comment := prepareMailerTest(t)
|
||||
|
||||
@ -108,8 +116,7 @@ func TestComposeIssueComment(t *testing.T) {
|
||||
setting.IncomingEmail.Enabled = true
|
||||
defer func() { setting.IncomingEmail.Enabled = false }()
|
||||
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
|
||||
bodyTemplates = template.Must(template.New("issue/comment").Parse(bodyTpl))
|
||||
prepareMailTemplates("issue/comment", subjectTpl, bodyTpl)
|
||||
|
||||
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
|
||||
msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
|
||||
@ -154,8 +161,7 @@ func TestComposeIssueComment(t *testing.T) {
|
||||
func TestMailMentionsComment(t *testing.T) {
|
||||
doer, _, issue, comment := prepareMailerTest(t)
|
||||
comment.Poster = doer
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
|
||||
bodyTemplates = template.Must(template.New("issue/comment").Parse(bodyTpl))
|
||||
prepareMailTemplates("issue/comment", subjectTpl, bodyTpl)
|
||||
mails := 0
|
||||
|
||||
defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) {
|
||||
@ -170,9 +176,7 @@ func TestMailMentionsComment(t *testing.T) {
|
||||
func TestComposeIssueMessage(t *testing.T) {
|
||||
doer, _, issue, _ := prepareMailerTest(t)
|
||||
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
|
||||
bodyTemplates = template.Must(template.New("issue/new").Parse(bodyTpl))
|
||||
|
||||
prepareMailTemplates("issue/new", subjectTpl, bodyTpl)
|
||||
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
|
||||
msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
|
||||
Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
|
||||
@ -201,15 +205,14 @@ func TestTemplateSelection(t *testing.T) {
|
||||
doer, repo, issue, comment := prepareMailerTest(t)
|
||||
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
|
||||
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse("issue/default/subject"))
|
||||
texttmpl.Must(subjectTemplates.New("issue/new").Parse("issue/new/subject"))
|
||||
texttmpl.Must(subjectTemplates.New("pull/comment").Parse("pull/comment/subject"))
|
||||
texttmpl.Must(subjectTemplates.New("issue/close").Parse("")) // Must default to fallback subject
|
||||
prepareMailTemplates("issue/default", "issue/default/subject", "issue/default/body")
|
||||
|
||||
bodyTemplates = template.Must(template.New("issue/default").Parse("issue/default/body"))
|
||||
template.Must(bodyTemplates.New("issue/new").Parse("issue/new/body"))
|
||||
template.Must(bodyTemplates.New("pull/comment").Parse("pull/comment/body"))
|
||||
template.Must(bodyTemplates.New("issue/close").Parse("issue/close/body"))
|
||||
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("issue/new").Parse("issue/new/subject"))
|
||||
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("pull/comment").Parse("pull/comment/subject"))
|
||||
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("issue/close").Parse("")) // Must default to a fallback subject
|
||||
template.Must(LoadedTemplates().BodyTemplates.New("issue/new").Parse("issue/new/body"))
|
||||
template.Must(LoadedTemplates().BodyTemplates.New("pull/comment").Parse("pull/comment/body"))
|
||||
template.Must(LoadedTemplates().BodyTemplates.New("issue/close").Parse("issue/close/body"))
|
||||
|
||||
expect := func(t *testing.T, msg *sender_service.Message, expSubject, expBody string) {
|
||||
subject := msg.ToMessage().GetGenHeader("Subject")
|
||||
@ -254,9 +257,7 @@ func TestTemplateServices(t *testing.T) {
|
||||
expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User,
|
||||
actionType activities_model.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string,
|
||||
) {
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject))
|
||||
bodyTemplates = template.Must(template.New("issue/default").Parse(tplBody))
|
||||
|
||||
prepareMailTemplates("issue/default", tplSubject, tplBody)
|
||||
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
|
||||
msg := testComposeIssueCommentMessage(t, &mailComment{
|
||||
Issue: issue, Doer: doer, ActionType: actionType,
|
||||
@ -523,8 +524,7 @@ func TestEmbedBase64Images(t *testing.T) {
|
||||
att2ImgBase64 := fmt.Sprintf(`<img src="%s"/>`, att2Base64)
|
||||
|
||||
t.Run("ComposeMessage", func(t *testing.T) {
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
|
||||
bodyTemplates = template.Must(template.New("issue/new").Parse(bodyTpl))
|
||||
prepareMailTemplates("issue/new", subjectTpl, bodyTpl)
|
||||
|
||||
issue.Content = fmt.Sprintf(`MSG-BEFORE <image src="attachments/%s"> MSG-AFTER`, att1.UUID)
|
||||
require.NoError(t, issues_model.UpdateIssueCols(t.Context(), issue, "content"))
|
||||
|
@ -39,7 +39,7 @@ func sendUserMail(language string, u *user_model.User, tpl templates.TplName, co
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
@ -90,7 +90,7 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
@ -118,7 +118,7 @@ func SendRegisterNotifyMail(u *user_model.User) {
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
@ -149,7 +149,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository)
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func NewContext(ctx context.Context) {
|
||||
sender = &sender_service.SMTPSender{}
|
||||
}
|
||||
|
||||
subjectTemplates, bodyTemplates = templates.Mailer(ctx)
|
||||
templates.LoadMailTemplates(ctx, &loadedTemplates)
|
||||
|
||||
mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*sender_service.Message) []*sender_service.Message {
|
||||
for _, msg := range items {
|
||||
|
@ -1,5 +1,4 @@
|
||||
// Copyright 2019 The Gitea Authors.
|
||||
// All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
@ -16,6 +15,7 @@ import (
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/pull"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@ -29,6 +29,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
@ -238,7 +239,7 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
|
||||
// markPullRequestAsMergeable checks if pull request is possible to leaving checking status,
|
||||
// and set to be either conflict or mergeable.
|
||||
func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullRequest) {
|
||||
// If status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
|
||||
// If the status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
|
||||
if pr.Status == issues_model.PullRequestStatusChecking {
|
||||
pr.Status = issues_model.PullRequestStatusMergeable
|
||||
}
|
||||
@ -257,6 +258,16 @@ func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullReques
|
||||
if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
|
||||
log.Error("Update[%-v]: %v", pr, err)
|
||||
}
|
||||
|
||||
// if there is a scheduled merge for this pull request, start the auto merge check (again)
|
||||
exist, _, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
|
||||
if err != nil {
|
||||
log.Error("GetScheduledMergeByPullID[%-v]: %v", pr, err)
|
||||
return
|
||||
} else if !exist {
|
||||
return
|
||||
}
|
||||
automergequeue.StartPRCheckAndAutoMerge(ctx, pr)
|
||||
}
|
||||
|
||||
// getMergeCommit checks if a pull request has been merged
|
||||
|
@ -1,5 +1,4 @@
|
||||
// Copyright 2019 The Gitea Authors.
|
||||
// All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
@ -11,11 +10,18 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/pull"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPullRequest_AddToTaskQueue(t *testing.T) {
|
||||
@ -63,6 +69,46 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
assert.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
|
||||
|
||||
prPatchCheckerQueue.ShutdownWait(5 * time.Second)
|
||||
prPatchCheckerQueue.ShutdownWait(time.Second)
|
||||
prPatchCheckerQueue = nil
|
||||
}
|
||||
|
||||
func TestMarkPullRequestAsMergeable(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", func(items ...string) []string { return nil })
|
||||
go prPatchCheckerQueue.Run()
|
||||
defer func() {
|
||||
prPatchCheckerQueue.ShutdownWait(time.Second)
|
||||
prPatchCheckerQueue = nil
|
||||
}()
|
||||
|
||||
addToQueueShaChan := make(chan string, 1)
|
||||
defer test.MockVariableValue(&automergequeue.AddToQueue, func(pr *issues_model.PullRequest, sha string) {
|
||||
addToQueueShaChan <- sha
|
||||
})()
|
||||
ctx := t.Context()
|
||||
_, _ = db.GetEngine(ctx).ID(2).Update(&issues_model.PullRequest{Status: issues_model.PullRequestStatusChecking})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
require.False(t, pr.HasMerged)
|
||||
require.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
|
||||
|
||||
err := pull.ScheduleAutoMerge(ctx, &user_model.User{ID: 99999}, pr.ID, repo_model.MergeStyleMerge, "test msg", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
exist, scheduleMerge, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.True(t, scheduleMerge.Doer.IsGhost())
|
||||
|
||||
markPullRequestAsMergeable(ctx, pr)
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
require.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status)
|
||||
|
||||
select {
|
||||
case sha := <-addToQueueShaChan:
|
||||
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", sha) // ref: refs/pull/3/head
|
||||
case <-time.After(1 * time.Second):
|
||||
assert.FailNow(t, "Timeout: nothing was added to automergequeue")
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
{{template "base/head" ctx.RootData}}
|
||||
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}">
|
||||
{{template "base/alert" .}}
|
||||
|
@ -2,13 +2,15 @@
|
||||
<div class="page-content devtest ui container">
|
||||
{{template "base/alert" .}}
|
||||
<div class="modal-buttons flex-text-block tw-flex-wrap"></div>
|
||||
<script type="module">
|
||||
<script>
|
||||
document.addEventListener('gitea:index-ready', () => {
|
||||
for (const el of $('.ui.modal:not([data-skip-button])')) {
|
||||
const $btn = $('<button class="ui button">').text(`${el.id}`).on('click', () => {
|
||||
$(el).modal({onApprove() {alert('confirmed')}}).modal('show');
|
||||
});
|
||||
$('.modal-buttons').append($btn);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="test-modal-form-1" class="ui mini modal">
|
||||
|
@ -45,7 +45,8 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<script type="module">
|
||||
<script>
|
||||
document.addEventListener('gitea:index-ready', () => {
|
||||
const $buttons = $('#devtest-button-samples').find('button.ui');
|
||||
|
||||
const $buttonStyles = $('input[name*="button-style"]');
|
||||
@ -53,6 +54,7 @@
|
||||
|
||||
const $buttonStates = $('input[name*="button-state"]');
|
||||
$buttonStates.on('click', () => $buttonStates.map((_, el) => $buttons.prop(el.value, el.checked)));
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
27
templates/devtest/mail-preview.tmpl
Normal file
27
templates/devtest/mail-preview.tmpl
Normal file
@ -0,0 +1,27 @@
|
||||
{{template "devtest/devtest-header"}}
|
||||
<div class="page-content devtest ui container">
|
||||
<div class="flex-text-block tw-flex-wrap">
|
||||
{{range $templateName := .MailTemplateNames}}
|
||||
<a class="ui button" href="?tmpl={{$templateName}}">{{$templateName}}</a>
|
||||
{{else}}
|
||||
<p>Mailer service is not enabled or no template is found</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .RenderMailTemplateName}}
|
||||
<div class="tw-my-2">
|
||||
<div>Preview of: {{.RenderMailTemplateName}}</div>
|
||||
<div>Subject: {{.RenderMailSubject}}</div>
|
||||
<iframe src="{{AppSubUrl}}/devtest/mail-preview/{{.RenderMailTemplateName}}" class="mail-preview-body"></iframe>
|
||||
<style>
|
||||
.mail-preview-body {
|
||||
border: 1px solid #ccc;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "devtest/devtest-footer"}}
|
3
templates/mail/auth/activate.mock.yml
Normal file
3
templates/mail/auth/activate.mock.yml
Normal file
@ -0,0 +1,3 @@
|
||||
DisplayName: User Display Name
|
||||
Code: The-Activation-Code
|
||||
ActiveCodeLives: 24h
|
@ -16,7 +16,7 @@
|
||||
<td class="author">
|
||||
<div class="tw-flex">
|
||||
{{$userName := .Author.Name}}
|
||||
{{if .User}}
|
||||
{{if and .User (gt .User.ID 0)}} /* User with id == 0 is a fake user from git author */
|
||||
{{if and .User.FullName DefaultShowFullName}}
|
||||
{{$userName = .User.FullName}}
|
||||
{{end}}
|
||||
|
@ -2,36 +2,29 @@
|
||||
{{if and .CanUseTimetracker (not .Repository.IsArchived)}}
|
||||
<div class="divider"></div>
|
||||
<div>
|
||||
<div class="ui dropdown full-width jump">
|
||||
<a class="fixed-text muted">
|
||||
<div>
|
||||
<strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong>
|
||||
{{if $.IsStopwatchRunning}}{{svg "octicon-stopwatch"}}{{end}}
|
||||
<div class="flex-text-block">
|
||||
<strong class="tw-flex-1">{{ctx.Locale.Tr "repo.issues.tracker"}}</strong>
|
||||
<button class="btn interact-fg show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.time_estimate_set"}}" data-modal="#issue-time-set-estimate-modal">
|
||||
{{svg "octicon-pencil"}}
|
||||
</button>
|
||||
</div>
|
||||
{{svg "octicon-gear"}}
|
||||
</a>
|
||||
<div class="menu">
|
||||
<a class="item issue-set-time-estimate show-modal" data-modal="#issue-time-set-estimate-modal">
|
||||
{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.time_estimate_set"}}
|
||||
</a>
|
||||
<div class="divider"></div>
|
||||
<div class="ui buttons tw-mt-2 tw-w-full">
|
||||
{{if $.IsStopwatchRunning}}
|
||||
<a class="item issue-stop-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/stop">
|
||||
<button class="ui button tw-flex-1 issue-stop-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/stop">
|
||||
{{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_stop"}}
|
||||
</a>
|
||||
<a class="item issue-cancel-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/cancel">
|
||||
{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_discard"}}
|
||||
</a>
|
||||
</button>
|
||||
<button class="ui icon button issue-cancel-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/cancel" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.timetracker_timer_discard"}}">
|
||||
{{svg "octicon-trash"}}
|
||||
</button>
|
||||
{{else}}
|
||||
<a class="item issue-start-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/start">
|
||||
<button class="ui button tw-flex-1 issue-start-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/start">
|
||||
{{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_start"}}
|
||||
</a>
|
||||
<a class="item issue-add-time show-modal" data-modal="#issue-time-manually-add-modal">
|
||||
{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_manually_add"}}
|
||||
</a>
|
||||
</button>
|
||||
<button class="ui icon button issue-add-time show-modal" data-modal="#issue-time-manually-add-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.timetracker_timer_manually_add"}}">
|
||||
{{svg "octicon-plus"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if and (not $.IsStopwatchRunning) .HasUserStopwatch}}
|
||||
<div class="ui warning message">{{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}}</div>
|
||||
@ -74,23 +67,19 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .WorkingUsers}}
|
||||
<div class="ui comments tw-mt-2">
|
||||
<div class="tw-mt-2">
|
||||
{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Hour)}}
|
||||
<div>
|
||||
</div>
|
||||
<div class="ui list flex-items-block">
|
||||
{{range $user, $trackedtime := .WorkingUsers}}
|
||||
<div class="comment tw-mt-2">
|
||||
<a class="avatar">
|
||||
{{ctx.AvatarUtils.Avatar $user}}
|
||||
</a>
|
||||
<div class="content">
|
||||
<div class="item tw-gap-3">
|
||||
{{template "shared/user/avatarlink" dict "user" $user}}
|
||||
<div>
|
||||
{{template "shared/user/authorlink" $user}}
|
||||
<div class="text">
|
||||
{{$trackedtime|Sec2Hour}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="text">{{$trackedtime|Sec2Hour}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"net/http/httptest"
|
||||
neturl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -100,6 +101,7 @@ func TestPackageNuGet(t *testing.T) {
|
||||
packageVersion := "1.0.3"
|
||||
packageAuthors := "KN4CK3R"
|
||||
packageDescription := "Gitea Test Package"
|
||||
isPrerelease := strings.Contains(packageVersion, "-")
|
||||
|
||||
symbolFilename := "test.pdb"
|
||||
symbolID := "d910bb6948bd4c6cb40155bcf52c3c94"
|
||||
@ -112,11 +114,17 @@ func TestPackageNuGet(t *testing.T) {
|
||||
packageOwners := "Package Owners"
|
||||
packageProjectURL := "https://gitea.io"
|
||||
packageReleaseNotes := "Package Release Notes"
|
||||
summary := "This is a test package."
|
||||
packageTags := "tag_1 tag_2 tag_3"
|
||||
packageTitle := "Package Title"
|
||||
packageDevelopmentDependency := true
|
||||
packageRequireLicenseAcceptance := true
|
||||
|
||||
dependencyCount := 1
|
||||
dependencyTargetFramework := ".NETStandard2.0"
|
||||
dependencyID := "Microsoft.CSharp"
|
||||
dependencyVersion := "4.5.0"
|
||||
|
||||
createNuspec := func(id, version string) string {
|
||||
return `<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
@ -133,12 +141,13 @@ func TestPackageNuGet(t *testing.T) {
|
||||
<projectUrl>` + packageProjectURL + `</projectUrl>
|
||||
<releaseNotes>` + packageReleaseNotes + `</releaseNotes>
|
||||
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||
<summary>` + summary + `</summary>
|
||||
<tags>` + packageTags + `</tags>
|
||||
<title>` + packageTitle + `</title>
|
||||
<version>` + version + `</version>
|
||||
<dependencies>
|
||||
<group targetFramework=".NETStandard2.0">
|
||||
<dependency id="Microsoft.CSharp" version="4.5.0" />
|
||||
<group targetFramework="` + dependencyTargetFramework + `">
|
||||
<dependency id="` + dependencyID + `" version="` + dependencyVersion + `" />
|
||||
</group>
|
||||
</dependencies>
|
||||
</metadata>
|
||||
@ -428,7 +437,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||
|
||||
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(610), pb.Size)
|
||||
assert.Equal(t, int64(633), pb.Size)
|
||||
case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion):
|
||||
assert.False(t, pf.IsLead)
|
||||
|
||||
@ -440,7 +449,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||
|
||||
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(996), pb.Size)
|
||||
assert.Equal(t, int64(1043), pb.Size)
|
||||
case symbolFilename:
|
||||
assert.False(t, pf.IsLead)
|
||||
|
||||
@ -747,17 +756,39 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||
assert.Equal(t, indexURL, result.RegistrationIndexURL)
|
||||
assert.Equal(t, 1, result.Count)
|
||||
assert.Len(t, result.Pages, 1)
|
||||
assert.Equal(t, indexURL, result.Pages[0].RegistrationPageURL)
|
||||
assert.Equal(t, packageVersion, result.Pages[0].Lower)
|
||||
assert.Equal(t, packageVersion, result.Pages[0].Upper)
|
||||
assert.Equal(t, 1, result.Pages[0].Count)
|
||||
assert.Len(t, result.Pages[0].Items, 1)
|
||||
assert.Equal(t, packageName, result.Pages[0].Items[0].CatalogEntry.ID)
|
||||
assert.Equal(t, packageVersion, result.Pages[0].Items[0].CatalogEntry.Version)
|
||||
assert.Equal(t, packageAuthors, result.Pages[0].Items[0].CatalogEntry.Authors)
|
||||
assert.Equal(t, packageDescription, result.Pages[0].Items[0].CatalogEntry.Description)
|
||||
assert.Equal(t, leafURL, result.Pages[0].Items[0].CatalogEntry.CatalogLeafURL)
|
||||
assert.Equal(t, contentURL, result.Pages[0].Items[0].CatalogEntry.PackageContentURL)
|
||||
|
||||
page := result.Pages[0]
|
||||
assert.Equal(t, indexURL, page.RegistrationPageURL)
|
||||
assert.Equal(t, packageVersion, page.Lower)
|
||||
assert.Equal(t, packageVersion, page.Upper)
|
||||
assert.Equal(t, 1, page.Count)
|
||||
assert.Len(t, page.Items, 1)
|
||||
|
||||
item := page.Items[0]
|
||||
assert.Equal(t, packageName, item.CatalogEntry.ID)
|
||||
assert.Equal(t, packageVersion, item.CatalogEntry.Version)
|
||||
assert.Equal(t, packageAuthors, item.CatalogEntry.Authors)
|
||||
assert.Equal(t, packageDescription, item.CatalogEntry.Description)
|
||||
assert.Equal(t, leafURL, item.CatalogEntry.CatalogLeafURL)
|
||||
assert.Equal(t, contentURL, item.CatalogEntry.PackageContentURL)
|
||||
assert.Equal(t, packageIconURL, item.CatalogEntry.IconURL)
|
||||
assert.Equal(t, packageLanguage, item.CatalogEntry.Language)
|
||||
assert.Equal(t, packageLicenseURL, item.CatalogEntry.LicenseURL)
|
||||
assert.Equal(t, packageProjectURL, item.CatalogEntry.ProjectURL)
|
||||
assert.Equal(t, packageReleaseNotes, item.CatalogEntry.ReleaseNotes)
|
||||
assert.Equal(t, packageRequireLicenseAcceptance, item.CatalogEntry.RequireLicenseAcceptance)
|
||||
assert.Equal(t, packageTags, item.CatalogEntry.Tags)
|
||||
assert.Equal(t, summary, item.CatalogEntry.Summary)
|
||||
assert.Equal(t, isPrerelease, item.CatalogEntry.IsPrerelease)
|
||||
assert.Len(t, item.CatalogEntry.DependencyGroups, dependencyCount)
|
||||
|
||||
dependencyGroup := item.CatalogEntry.DependencyGroups[0]
|
||||
assert.Equal(t, dependencyTargetFramework, dependencyGroup.TargetFramework)
|
||||
assert.Len(t, dependencyGroup.Dependencies, dependencyCount)
|
||||
|
||||
dependency := dependencyGroup.Dependencies[0]
|
||||
assert.Equal(t, dependencyID, dependency.ID)
|
||||
assert.Equal(t, dependencyVersion, dependency.Range)
|
||||
})
|
||||
|
||||
t.Run("RegistrationLeaf", func(t *testing.T) {
|
||||
@ -789,7 +820,8 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||
assert.Equal(t, packageTags, result.Properties.Tags)
|
||||
assert.Equal(t, packageTitle, result.Properties.Title)
|
||||
|
||||
assert.Equal(t, "Microsoft.CSharp:4.5.0:.NETStandard2.0", result.Properties.Dependencies)
|
||||
packageVersion := strings.Join([]string{dependencyID, dependencyVersion, dependencyTargetFramework}, ":")
|
||||
assert.Equal(t, packageVersion, result.Properties.Dependencies)
|
||||
})
|
||||
|
||||
t.Run("v3", func(t *testing.T) {
|
||||
@ -803,8 +835,30 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||
DecodeJSON(t, resp, &result)
|
||||
|
||||
assert.Equal(t, leafURL, result.RegistrationLeafURL)
|
||||
assert.Equal(t, contentURL, result.PackageContentURL)
|
||||
assert.Equal(t, indexURL, result.RegistrationIndexURL)
|
||||
assert.Equal(t, packageAuthors, result.CatalogEntry.Authors)
|
||||
assert.Equal(t, packageCopyright, result.CatalogEntry.Copyright)
|
||||
|
||||
dependencyGroup := result.CatalogEntry.DependencyGroups[0]
|
||||
assert.Equal(t, dependencyTargetFramework, dependencyGroup.TargetFramework)
|
||||
assert.Len(t, dependencyGroup.Dependencies, dependencyCount)
|
||||
|
||||
dependency := dependencyGroup.Dependencies[0]
|
||||
assert.Equal(t, dependencyID, dependency.ID)
|
||||
assert.Equal(t, dependencyVersion, dependency.Range)
|
||||
|
||||
assert.Equal(t, packageDescription, result.CatalogEntry.Description)
|
||||
assert.Equal(t, packageID, result.CatalogEntry.ID)
|
||||
assert.Equal(t, packageIconURL, result.CatalogEntry.IconURL)
|
||||
assert.Equal(t, isPrerelease, result.CatalogEntry.IsPrerelease)
|
||||
assert.Equal(t, packageLanguage, result.CatalogEntry.Language)
|
||||
assert.Equal(t, packageLicenseURL, result.CatalogEntry.LicenseURL)
|
||||
assert.Equal(t, contentURL, result.PackageContentURL)
|
||||
assert.Equal(t, packageProjectURL, result.CatalogEntry.ProjectURL)
|
||||
assert.Equal(t, packageRequireLicenseAcceptance, result.CatalogEntry.RequireLicenseAcceptance)
|
||||
assert.Equal(t, summary, result.CatalogEntry.Summary)
|
||||
assert.Equal(t, packageTags, result.CatalogEntry.Tags)
|
||||
assert.Equal(t, packageVersion, result.CatalogEntry.Version)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -489,40 +490,60 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
|
||||
}
|
||||
|
||||
func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns, protectedFilePatterns string) func(t *testing.T) {
|
||||
return doProtectBranchExt(ctx, branch, doProtectBranchOptions{
|
||||
UserToWhitelistPush: userToWhitelistPush,
|
||||
UserToWhitelistForcePush: userToWhitelistForcePush,
|
||||
UnprotectedFilePatterns: unprotectedFilePatterns,
|
||||
ProtectedFilePatterns: protectedFilePatterns,
|
||||
})
|
||||
}
|
||||
|
||||
type doProtectBranchOptions struct {
|
||||
UserToWhitelistPush, UserToWhitelistForcePush, UnprotectedFilePatterns, ProtectedFilePatterns string
|
||||
|
||||
StatusCheckPatterns []string
|
||||
}
|
||||
|
||||
func doProtectBranchExt(ctx APITestContext, ruleName string, opts doProtectBranchOptions) func(t *testing.T) {
|
||||
// We are going to just use the owner to set the protection.
|
||||
return func(t *testing.T) {
|
||||
csrf := GetUserCSRFToken(t, ctx.Session)
|
||||
|
||||
formData := map[string]string{
|
||||
"_csrf": csrf,
|
||||
"rule_name": branch,
|
||||
"unprotected_file_patterns": unprotectedFilePatterns,
|
||||
"protected_file_patterns": protectedFilePatterns,
|
||||
"rule_name": ruleName,
|
||||
"unprotected_file_patterns": opts.UnprotectedFilePatterns,
|
||||
"protected_file_patterns": opts.ProtectedFilePatterns,
|
||||
}
|
||||
|
||||
if userToWhitelistPush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistPush)
|
||||
if opts.UserToWhitelistPush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, opts.UserToWhitelistPush)
|
||||
assert.NoError(t, err)
|
||||
formData["whitelist_users"] = strconv.FormatInt(user.ID, 10)
|
||||
formData["enable_push"] = "whitelist"
|
||||
formData["enable_whitelist"] = "on"
|
||||
}
|
||||
|
||||
if userToWhitelistForcePush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistForcePush)
|
||||
if opts.UserToWhitelistForcePush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, opts.UserToWhitelistForcePush)
|
||||
assert.NoError(t, err)
|
||||
formData["force_push_allowlist_users"] = strconv.FormatInt(user.ID, 10)
|
||||
formData["enable_force_push"] = "whitelist"
|
||||
formData["enable_force_push_allowlist"] = "on"
|
||||
}
|
||||
|
||||
if len(opts.StatusCheckPatterns) > 0 {
|
||||
formData["enable_status_check"] = "on"
|
||||
formData["status_check_contexts"] = strings.Join(opts.StatusCheckPatterns, "\n")
|
||||
}
|
||||
|
||||
// Send the request to update branch protection settings
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), formData)
|
||||
ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
// Check if master branch has been locked successfully
|
||||
// Check if the "master" branch has been locked successfully
|
||||
flashMsg := ctx.Session.GetCookieFlashMessage()
|
||||
assert.Equal(t, `Branch protection for rule "`+branch+`" has been updated.`, flashMsg.SuccessMsg)
|
||||
assert.Equal(t, `Branch protection for rule "`+ruleName+`" has been updated.`, flashMsg.SuccessMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -688,6 +709,10 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
|
||||
|
||||
ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// automerge will merge immediately if the PR is mergeable and there is no "status check" because no status check also means "all checks passed"
|
||||
// so we must set up a status check to test the auto merge feature
|
||||
doProtectBranchExt(ctx, "protected", doProtectBranchOptions{StatusCheckPatterns: []string{"*"}})(t)
|
||||
|
||||
t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected"))
|
||||
t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
|
||||
t.Run("GenerateCommit", func(t *testing.T) {
|
||||
@ -728,7 +753,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
|
||||
|
||||
// Cancel not existing auto merge
|
||||
ctx.ExpectedCode = http.StatusNotFound
|
||||
t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
t.Run("CancelAutoMergePRNotExist", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
|
||||
// Add auto merge request
|
||||
ctx.ExpectedCode = http.StatusCreated
|
||||
|
@ -35,6 +35,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/services/automerge"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
|
||||
@ -727,7 +728,7 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
|
||||
|
||||
// add protected branch for commit status
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
// Change master branch to protected
|
||||
// Change the "master" branch to "protected"
|
||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
|
||||
"_csrf": csrf,
|
||||
"rule_name": "master",
|
||||
@ -737,10 +738,22 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
oldAutoMergeAddToQueue := automergequeue.AddToQueue
|
||||
addToQueueShaChan := make(chan string, 1)
|
||||
automergequeue.AddToQueue = func(pr *issues_model.PullRequest, sha string) {
|
||||
addToQueueShaChan <- sha
|
||||
}
|
||||
// first time insert automerge record, return true
|
||||
scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, scheduled)
|
||||
// and the pr should be added to automergequeue, in case it is already "mergeable"
|
||||
select {
|
||||
case <-addToQueueShaChan:
|
||||
case <-time.After(time.Second):
|
||||
assert.FailNow(t, "Timeout: nothing was added to automergequeue")
|
||||
}
|
||||
automergequeue.AddToQueue = oldAutoMergeAddToQueue
|
||||
|
||||
// second time insert automerge record, return false because it does exist
|
||||
scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
|
||||
@ -775,13 +788,11 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// realod pr again
|
||||
assert.Eventually(t, func() bool {
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
||||
assert.True(t, pr.HasMerged)
|
||||
return pr.HasMerged
|
||||
}, 2*time.Second, 100*time.Millisecond)
|
||||
assert.NotEmpty(t, pr.MergedCommitID)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
|
||||
})
|
||||
}
|
||||
|
@ -366,8 +366,8 @@ It needs some tricks to tweak the left/right borders with active state */
|
||||
|
||||
.ui.buttons .button {
|
||||
border-right: none;
|
||||
flex: 1 0 auto;
|
||||
border-radius: 0;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.ui.buttons .button:first-child {
|
||||
|
@ -138,7 +138,14 @@ function initDiffHeaderPopup() {
|
||||
btn.setAttribute('data-header-popup-initialized', '');
|
||||
const popup = btn.nextElementSibling;
|
||||
if (!popup?.matches('.tippy-target')) throw new Error('Popup element not found');
|
||||
createTippy(btn, {content: popup, theme: 'menu', placement: 'bottom', trigger: 'click', interactive: true, hideOnClick: true});
|
||||
createTippy(btn, {
|
||||
content: popup,
|
||||
theme: 'menu',
|
||||
placement: 'bottom-end',
|
||||
trigger: 'click',
|
||||
interactive: true,
|
||||
hideOnClick: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,3 +175,5 @@ const initDur = performance.now() - initStartTime;
|
||||
if (initDur > 500) {
|
||||
console.error(`slow init functions took ${initDur.toFixed(3)}ms`);
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('gitea:index-ready'));
|
||||
|
@ -11,4 +11,5 @@ function initDevtestToast() {
|
||||
}
|
||||
}
|
||||
|
||||
// NOTICE: keep in mind that this file is not in "index.js", they do not share the same module system.
|
||||
initDevtestToast();
|
||||
|
Loading…
x
Reference in New Issue
Block a user