From ddfa2e4a3ef18cdef0f66d69af2c581ffe095f9b Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 7 Jul 2025 00:41:20 +0000 Subject: [PATCH 1/9] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 99c8b56f60..49ce14aded 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -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 From f88800d54505f30dbae089d446e292f176222d99 Mon Sep 17 00:00:00 2001 From: Scion Date: Mon, 7 Jul 2025 03:43:58 -0700 Subject: [PATCH 2/9] Improve NuGet API Parity (#21291) (#34940) Fixes #21291, allowing icons and other missing attributes to appear for NuGet packages from inside Visual Studio like they do with GitHub Nuget packages. Adds additional NuGet package information, particularly `IconURL`, to bring the Gitea NuGet API more in-line with GitHub's NuGet API. ref: https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource --- models/packages/package_version.go | 8 ++ modules/packages/nuget/metadata.go | 3 + routers/api/packages/nuget/api_v3.go | 144 +++++++++++++------ tests/integration/api_packages_nuget_test.go | 88 +++++++++--- 4 files changed, 184 insertions(+), 59 deletions(-) diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 5672e0efbf..0a478c0323 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -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) diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index a122590bf1..513b4dd2b9 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -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, diff --git a/routers/api/packages/nuget/api_v3.go b/routers/api/packages/nuget/api_v3.go index 2fe25dc0f8..3262f2d9af 100644 --- a/routers/api/packages/nuget/api_v3.go +++ b/routers/api/packages/nuget/api_v3.go @@ -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 @@ -109,15 +117,24 @@ func createRegistrationIndexPageItem(l *linkBuilder, pd *packages_model.PackageD RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version), 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, - DependencyGroups: createDependencyGroups(pd), + CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version), + 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: 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(), }, } } @@ -145,22 +162,42 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe // https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf 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"` + RegistrationLeafURL string `json:"@id"` + Type []string `json:"@type"` + PackageContentURL string `json:"packageContent"` + 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,13 +225,24 @@ type SearchResultResponse struct { // https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result type SearchResult struct { - ID string `json:"id"` - Version string `json:"version"` - Versions []*SearchResultVersion `json:"versions"` - Description string `json:"description"` - Authors string `json:"authors"` - ProjectURL string `json:"projectURL"` - RegistrationIndexURL string `json:"registration"` + 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"` + RegistrationIndexURL string `json:"registration"` } // https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result @@ -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{ - ID: latest.Package.Name, - Version: latest.Version.Version, - Versions: versions, - Description: metadata.Description, - Authors: metadata.Authors, - ProjectURL: metadata.ProjectURL, - RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name), + 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, + RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name), } } diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go index 65b1b9845a..529b540062 100644 --- a/tests/integration/api_packages_nuget_test.go +++ b/tests/integration/api_packages_nuget_test.go @@ -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 ` @@ -133,12 +141,13 @@ func TestPackageNuGet(t *testing.T) { ` + packageProjectURL + ` ` + packageReleaseNotes + ` true + ` + summary + ` ` + packageTags + ` ` + packageTitle + ` ` + version + ` - - + + @@ -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) }) }) }) From 08682212ab631572bc680251ceb4daa7bb4e7b9d Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 8 Jul 2025 00:37:29 +0000 Subject: [PATCH 3/9] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.ini | 1 + options/locale/locale_pt-PT.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 49ce14aded..965cce0c68 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -2355,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 diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 2ebea36af9..f10bef0687 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -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 From 3763c2ae28ebfa539cefb7ce0830cf5ee6c6f1df Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 8 Jul 2025 16:59:31 +0800 Subject: [PATCH 4/9] Refactor time tracker UI (#34983) Although we decided to "reduce the button amount" on the side bar, not only one user reported that the "time tracker dropdown" is not easy to use. So the best we can do at the moment is: move the buttons to the sidebar again. Fix #34979 --- templates/devtest/fomantic-modal.tmpl | 16 ++-- templates/devtest/gitea-ui.tmpl | 14 ++-- .../issue/sidebar/stopwatch_timetracker.tmpl | 77 ++++++++----------- web_src/css/modules/button.css | 2 +- web_src/js/index-domready.ts | 2 + web_src/js/standalone/devtest.ts | 1 + 6 files changed, 54 insertions(+), 58 deletions(-) diff --git a/templates/devtest/fomantic-modal.tmpl b/templates/devtest/fomantic-modal.tmpl index 838c6893a4..8e769790b2 100644 --- a/templates/devtest/fomantic-modal.tmpl +++ b/templates/devtest/fomantic-modal.tmpl @@ -2,13 +2,15 @@
{{template "base/alert" .}} - -
diff --git a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl index ab8600b068..6168b06e17 100644 --- a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl +++ b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl @@ -2,35 +2,28 @@ {{if and .CanUseTimetracker (not .Repository.IsArchived)}}
- {{end}} {{if .WorkingUsers}} -
+
{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Hour)}} -
- {{range $user, $trackedtime := .WorkingUsers}} -
- - {{ctx.AvatarUtils.Avatar $user}} - -
- {{template "shared/user/authorlink" $user}} -
- {{$trackedtime|Sec2Hour}} -
-
+
+
+ {{range $user, $trackedtime := .WorkingUsers}} +
+ {{template "shared/user/avatarlink" dict "user" $user}} +
+ {{template "shared/user/authorlink" $user}} +
{{$trackedtime|Sec2Hour}}
- {{end}} -
+
+ {{end}}
{{end}} {{end}} diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css index b105bb5de2..8e3309474b 100644 --- a/web_src/css/modules/button.css +++ b/web_src/css/modules/button.css @@ -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 { diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index 4d7ab98db0..ca18d1e828 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -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')); diff --git a/web_src/js/standalone/devtest.ts b/web_src/js/standalone/devtest.ts index e6baf6c9ce..faa38dc467 100644 --- a/web_src/js/standalone/devtest.ts +++ b/web_src/js/standalone/devtest.ts @@ -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(); From 4e10adc871363ccd099867563a06a55bb50b296a Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 8 Jul 2025 22:51:16 +0800 Subject: [PATCH 5/9] Start automerge check again after the conflict check and the schedule (#34989) Fix #34988 Co-authored-by: posativ --- models/pull/automerge.go | 7 ++- services/automerge/automerge.go | 55 ++++++----------------- services/automerge/notify.go | 3 +- services/automergequeue/automergequeue.go | 49 ++++++++++++++++++++ services/pull/check.go | 17 +++++-- services/pull/check_test.go | 52 +++++++++++++++++++-- tests/integration/git_general_test.go | 47 ++++++++++++++----- tests/integration/pull_merge_test.go | 25 ++++++++--- 8 files changed, 187 insertions(+), 68 deletions(-) create mode 100644 services/automergequeue/automergequeue.go diff --git a/models/pull/automerge.go b/models/pull/automerge.go index 3cafacc3a4..7f940a9849 100644 --- a/models/pull/automerge.go +++ b/models/pull/automerge.go @@ -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 } diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index 0520a097d3..1a37d70e5a 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -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 { diff --git a/services/automerge/notify.go b/services/automerge/notify.go index b6bbca333b..8a1bb5fc90 100644 --- a/services/automerge/notify.go +++ b/services/automerge/notify.go @@ -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) { diff --git a/services/automergequeue/automergequeue.go b/services/automergequeue/automergequeue.go new file mode 100644 index 0000000000..cdf257e6c8 --- /dev/null +++ b/services/automergequeue/automergequeue.go @@ -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) +} diff --git a/services/pull/check.go b/services/pull/check.go index 5d8990aa00..bf6c5fa1c4 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -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 diff --git a/services/pull/check_test.go b/services/pull/check_test.go index fa3a676ef1..eb66615dcf 100644 --- a/services/pull/check_test.go +++ b/services/pull/check_test.go @@ -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") + } +} diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go index 3b0f9589d2..e72b7b4ff1 100644 --- a/tests/integration/git_general_test.go +++ b/tests/integration/git_general_test.go @@ -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,13 +753,13 @@ 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 t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - // Can not create schedule twice + // Cannot create schedule twice ctx.ExpectedCode = http.StatusConflict t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 73b4c22070..897a78cef4 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -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 - pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) - assert.True(t, pr.HasMerged) + assert.Eventually(t, func() bool { + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) + return pr.HasMerged + }, 2*time.Second, 100*time.Millisecond) assert.NotEmpty(t, pr.MergedCommitID) - unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID}) }) } From d3d357a4a4bfdd3a2ebd81021e5d62b1cf5cd59d Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 8 Jul 2025 23:44:14 +0200 Subject: [PATCH 6/9] Tweak placement of diff file menu (#34999) Small tweak for better visual placement. Before: Screenshot 2025-07-08 at 18 16 51 After: Screenshot 2025-07-08 at 18 16 34 Placement matches the "..." button above. --- web_src/js/features/repo-diff.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index ad1da5c2fa..bde7ec0324 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -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, + }); } } From f1b78f3cdd5ae23fd679c9c1a85f4f1e85e92857 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 9 Jul 2025 06:49:30 +0800 Subject: [PATCH 7/9] Fix bug when displaying git user avatar in commits list (#35003) A quick fix for #34991 `ValidateCommitsWithEmails` will create a fake user for a git commit user with a related Gitea user. The UI should not display a link for such users. --- templates/repo/commits_list.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 959f2a9398..9dae6594b9 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -16,7 +16,7 @@
{{$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}} From 2cc33686104da45b426eb4bb4892468f612ac404 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 9 Jul 2025 00:38:25 +0000 Subject: [PATCH 8/9] [skip ci] Updated translations via Crowdin --- options/locale/locale_zh-CN.ini | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 2a97941d6b..f2a3d29bec 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -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=您不能直接编辑此仓库。 更改将写入您的派生仓库 %s,以便您可以创建一个拉取请求。 -editor.fork_not_editable=你已经派生了这个仓库,但是你的分叉是不可编辑的。 +editor.fork_create=派生仓库以请求变更 +editor.fork_create_description=您不能直接编辑此仓库。您可以派生此仓库,进行编辑并创建一个合并请求。 +editor.fork_edit_description=您不能直接编辑此仓库。 更改将写入您的派生仓库 %s,以便您可以创建一个合并请求。 +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=没有找到匹配的文件 From 55f350542ce6db6621ceed987d3d11b8ab5dd2dd Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 9 Jul 2025 10:25:25 +0800 Subject: [PATCH 9/9] Refactor mail template and support preview (#34990) --- modules/templates/mailer.go | 33 ++++++++++----- routers/web/devtest/mail_preview.go | 58 +++++++++++++++++++++++++++ routers/web/web.go | 2 + services/mailer/mail.go | 15 ++++--- services/mailer/mail_issue_common.go | 10 ++--- services/mailer/mail_release.go | 2 +- services/mailer/mail_repo.go | 2 +- services/mailer/mail_team_invite.go | 2 +- services/mailer/mail_test.go | 40 +++++++++--------- services/mailer/mail_user.go | 8 ++-- services/mailer/mailer.go | 2 +- templates/devtest/devtest-header.tmpl | 1 + templates/devtest/mail-preview.tmpl | 27 +++++++++++++ templates/mail/auth/activate.mock.yml | 3 ++ 14 files changed, 156 insertions(+), 49 deletions(-) create mode 100644 routers/web/devtest/mail_preview.go create mode 100644 templates/devtest/mail-preview.tmpl create mode 100644 templates/mail/auth/activate.mock.yml diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go index 310d645328..c43b760777 100644 --- a/modules/templates/mailer.go +++ b/modules/templates/mailer.go @@ -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) { - subjectTemplates := texttmpl.New("") - bodyTemplates := template.New("") - - subjectTemplates.Funcs(mailSubjectTextFuncMap()) - bodyTemplates.Funcs(NewFuncMap()) - +// 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()) + 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 } diff --git a/routers/web/devtest/mail_preview.go b/routers/web/devtest/mail_preview.go new file mode 100644 index 0000000000..79dd441eab --- /dev/null +++ b/routers/web/devtest/mail_preview.go @@ -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") +} diff --git a/routers/web/web.go b/routers/web/web.go index 66c3a2da09..ddea468d17 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -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) diff --git a/services/mailer/mail.go b/services/mailer/mail.go index aa51cbdbcf..b7602e0321 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -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 { diff --git a/services/mailer/mail_issue_common.go b/services/mailer/mail_issue_common.go index ebfd52162c..107f57772c 100644 --- a/services/mailer/mail_issue_common.go +++ b/services/mailer/mail_issue_common.go @@ -119,7 +119,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 @@ -134,7 +134,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) } @@ -260,14 +260,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" diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index bfff73c39c..fd97fb5312 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -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 } diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go index b6b2d5ca07..1ec7995ab9 100644 --- a/services/mailer/mail_repo.go +++ b/services/mailer/mail_repo.go @@ -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 } diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index f4aa788dec..034dc14e3d 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -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 } diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index b15949f352..3996796beb 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -25,6 +25,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" @@ -95,6 +96,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) @@ -107,8 +115,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{ @@ -153,8 +160,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) { @@ -169,9 +175,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, @@ -200,15 +204,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") @@ -253,9 +256,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, @@ -512,8 +513,7 @@ func TestEmbedBase64Images(t *testing.T) { att2ImgBase64 := fmt.Sprintf(``, 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 MSG-AFTER`, att1.UUID) require.NoError(t, issues_model.UpdateIssueCols(t.Context(), issue, "content")) diff --git a/services/mailer/mail_user.go b/services/mailer/mail_user.go index 5a200a5fa7..68df81f6a3 100644 --- a/services/mailer/mail_user.go +++ b/services/mailer/mail_user.go @@ -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 } diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go index bcd4facca9..db00aac4f1 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -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 { diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index ee08545640..0775dccc2d 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,2 +1,3 @@ {{template "base/head" ctx.RootData}} +{{template "base/alert" .}} diff --git a/templates/devtest/mail-preview.tmpl b/templates/devtest/mail-preview.tmpl new file mode 100644 index 0000000000..9a3d792904 --- /dev/null +++ b/templates/devtest/mail-preview.tmpl @@ -0,0 +1,27 @@ +{{template "devtest/devtest-header"}} +
+
+ {{range $templateName := .MailTemplateNames}} + {{$templateName}} + {{else}} +

Mailer service is not enabled or no template is found

+ {{end}} +
+ + {{if .RenderMailTemplateName}} +
+
Preview of: {{.RenderMailTemplateName}}
+
Subject: {{.RenderMailSubject}}
+ + +
+ {{end}} +
+{{template "devtest/devtest-footer"}} diff --git a/templates/mail/auth/activate.mock.yml b/templates/mail/auth/activate.mock.yml new file mode 100644 index 0000000000..f5519a6f6c --- /dev/null +++ b/templates/mail/auth/activate.mock.yml @@ -0,0 +1,3 @@ +DisplayName: User Display Name +Code: The-Activation-Code +ActiveCodeLives: 24h