From 89f31f79fde96f6cb685ea24a523344f66c5ecf1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 23 Dec 2024 11:21:11 +0800 Subject: [PATCH 1/5] Fix outdated tmpl code (#32953) Some PRs were before tmpl ctx refactoring and used outdated code --- templates/package/metadata/arch.tmpl | 2 +- templates/repo/issue/view_content/comments.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl index 2aea036ec2dd2..62308e4aa0bc1 100644 --- a/templates/package/metadata/arch.tmpl +++ b/templates/package/metadata/arch.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "arch"}} - {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} {{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 3f9af180275c8..9fdbf45939060 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -693,7 +693,7 @@ {{else if eq .Type 38}}
{{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}} + {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} {{$timeStr := .Content|TimeEstimateString}} From bd5d1341d43a94ac6b5f646eb9d995a48ba844e1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 23 Dec 2024 15:07:12 +0800 Subject: [PATCH 2/5] Fix commit range paging (#32944) --- modules/git/repo_commit.go | 6 ++---- modules/git/repo_commit_test.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 9ffadb833d8f6..647894bb21346 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -216,8 +216,6 @@ type CommitsByFileAndRangeOptions struct { // CommitsByFileAndRange return the commits according revision file and the page func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { - skip := (opts.Page - 1) * setting.Git.CommitsRangeSize - stdoutReader, stdoutWriter := io.Pipe() defer func() { _ = stdoutReader.Close() @@ -226,8 +224,8 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) go func() { stderr := strings.Builder{} gitCmd := NewCommand(repo.Ctx, "rev-list"). - AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize*opts.Page). - AddOptionFormat("--skip=%d", skip) + AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). + AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize) gitCmd.AddDynamicArguments(opts.Revision) if opts.Not != "" { diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 4c26fa2a486bf..e9f469accdf0c 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -8,7 +8,11 @@ import ( "path/filepath" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepository_GetCommitBranches(t *testing.T) { @@ -126,3 +130,21 @@ func TestGetRefCommitID(t *testing.T) { } } } + +func TestCommitsByFileAndRange(t *testing.T) { + defer test.MockVariableValue(&setting.Git.CommitsRangeSize, 2)() + + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path) + require.NoError(t, err) + defer bareRepo1.Close() + + // "foo" has 3 commits in "master" branch + commits, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1}) + require.NoError(t, err) + assert.Len(t, commits, 2) + + commits, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2}) + require.NoError(t, err) + assert.Len(t, commits, 1) +} From 7553ae1a57a2d017b8bb1024ad95b6918716ddb2 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 23 Dec 2024 17:59:16 +0800 Subject: [PATCH 3/5] Use env GITEA_RUNNER_REGISTRATION_TOKEN as global runner token (#32946) Fix #23703 When Gitea starts, it reads GITEA_RUNNER_REGISTRATION_TOKEN or GITEA_RUNNER_REGISTRATION_TOKEN_FILE to add registration token. --- models/actions/runner_token.go | 21 +++--- options/locale/locale_en-US.ini | 1 + routers/init.go | 2 +- routers/web/shared/actions/runners.go | 3 +- routers/web/web.go | 2 +- services/actions/init.go | 51 ++++++++++++++- services/actions/init_test.go | 80 +++++++++++++++++++++++ templates/shared/actions/runner_list.tmpl | 10 ++- 8 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 services/actions/init_test.go diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go index fd6ba7ecadb3f..1eab5efcce70e 100644 --- a/models/actions/runner_token.go +++ b/models/actions/runner_token.go @@ -10,6 +10,7 @@ import ( "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/base" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) @@ -51,7 +52,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist) + return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist) } return &runnerToken, nil } @@ -68,19 +69,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string return err } -// NewRunnerToken creates a new active runner token and invalidate all old tokens +// NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens // ownerID will be ignored and treated as 0 if repoID is non-zero. -func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { +func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) { if ownerID != 0 && repoID != 0 { // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. // Remove OwnerID to avoid confusion; it's not worth returning an error here. ownerID = 0 } - token, err := util.CryptoRandomString(40) - if err != nil { - return nil, err - } runnerToken := &ActionRunnerToken{ OwnerID: ownerID, RepoID: repoID, @@ -95,11 +92,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo return err } - _, err = db.GetEngine(ctx).Insert(runnerToken) + _, err := db.GetEngine(ctx).Insert(runnerToken) return err }) } +func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { + token, err := util.CryptoRandomString(40) + if err != nil { + return nil, err + } + return NewRunnerTokenWithValue(ctx, ownerID, repoID, token) +} + // GetLatestRunnerToken returns the latest runner token func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { if ownerID != 0 && repoID != 0 { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index aa01e8699a16a..ea4d36fb1ca7d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3722,6 +3722,7 @@ runners.status.active = Active runners.status.offline = Offline runners.version = Version runners.reset_registration_token = Reset registration token +runners.reset_registration_token_confirm = Would you like to invalidate the current token and generate a new one? runners.reset_registration_token_success = Runner registration token reset successfully runs.all_workflows = All Workflows diff --git a/routers/init.go b/routers/init.go index 2091f5967acac..98ce1bc4c9858 100644 --- a/routers/init.go +++ b/routers/init.go @@ -171,7 +171,7 @@ func InitWebInstalled(ctx context.Context) { auth.Init() mustInit(svg.Init) - actions_service.Init() + mustInitCtx(ctx, actions_service.Init) mustInit(repo_service.InitLicenseClassifier) diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index f38933226b883..6d77bdd2fab74 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -136,9 +136,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r ctx.ServerError("ResetRunnerRegistrationToken", err) return } - ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success")) - ctx.Redirect(redirectTo) + ctx.JSONRedirect(redirectTo) } // RunnerDeletePost response for deleting a runner diff --git a/routers/web/web.go b/routers/web/web.go index aa37d4dc10528..e1005aae440c1 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -463,7 +463,7 @@ func registerRoutes(m *web.Router) { m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit). Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost) m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost) - m.Get("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken) + m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken) }) } diff --git a/services/actions/init.go b/services/actions/init.go index 0f49cb629784f..7136da05ed8f3 100644 --- a/services/actions/init.go +++ b/services/actions/init.go @@ -4,23 +4,68 @@ package actions import ( + "context" + "errors" + "fmt" + "os" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) -func Init() { +func initGlobalRunnerToken(ctx context.Context) error { + // use the same env name as the runner, for consistency + token := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN") + tokenFile := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE") + if token != "" && tokenFile != "" { + return errors.New("both GITEA_RUNNER_REGISTRATION_TOKEN and GITEA_RUNNER_REGISTRATION_TOKEN_FILE are set, only one can be used") + } + if tokenFile != "" { + file, err := os.ReadFile(tokenFile) + if err != nil { + return fmt.Errorf("unable to read GITEA_RUNNER_REGISTRATION_TOKEN_FILE: %w", err) + } + token = strings.TrimSpace(string(file)) + } + if token == "" { + return nil + } + + if len(token) < 32 { + return errors.New("GITEA_RUNNER_REGISTRATION_TOKEN must be at least 32 random characters") + } + + existing, err := actions_model.GetRunnerToken(ctx, token) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return fmt.Errorf("unable to check existing token: %w", err) + } + if existing != nil { + if !existing.IsActive { + log.Warn("The token defined by GITEA_RUNNER_REGISTRATION_TOKEN is already invalidated, please use the latest one from web UI") + } + return nil + } + _, err = actions_model.NewRunnerTokenWithValue(ctx, 0, 0, token) + return err +} + +func Init(ctx context.Context) error { if !setting.Actions.Enabled { - return + return nil } jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler) if jobEmitterQueue == nil { - log.Fatal("Unable to create actions_ready_job queue") + return errors.New("unable to create actions_ready_job queue") } go graceful.GetManager().RunWithCancel(jobEmitterQueue) notify_service.RegisterNotifier(NewNotifier()) + return initGlobalRunnerToken(ctx) } diff --git a/services/actions/init_test.go b/services/actions/init_test.go new file mode 100644 index 0000000000000..59c321ccd771e --- /dev/null +++ b/services/actions/init_test.go @@ -0,0 +1,80 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "os" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + FixtureFiles: []string{"action_runner_token.yml"}, + }) + os.Exit(m.Run()) +} + +func TestInitToken(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("NoToken", func(t *testing.T) { + _, _ = db.Exec(db.DefaultContext, "DELETE FROM action_runner_token") + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "") + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "") + err := initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + notEmpty, err := db.IsTableNotEmpty(&actions_model.ActionRunnerToken{}) + require.NoError(t, err) + assert.False(t, notEmpty) + }) + + t.Run("EnvToken", func(t *testing.T) { + tokenValue, _ := util.CryptoRandomString(32) + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", tokenValue) + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "") + err := initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.True(t, token.IsActive) + + // init with the same token again, should not create a new token + err = initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + token2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.Equal(t, token.ID, token2.ID) + assert.True(t, token.IsActive) + }) + + t.Run("EnvFileToken", func(t *testing.T) { + tokenValue, _ := util.CryptoRandomString(32) + f := t.TempDir() + "/token" + _ = os.WriteFile(f, []byte(tokenValue), 0o644) + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "") + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", f) + err := initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.True(t, token.IsActive) + + // if the env token is invalidated by another new token, then it shouldn't be active anymore + _, err = actions_model.NewRunnerToken(db.DefaultContext, 0, 0) + require.NoError(t, err) + token = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.False(t, token.IsActive) + }) + + t.Run("InvalidToken", func(t *testing.T) { + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "abc") + err := initGlobalRunnerToken(db.DefaultContext) + assert.ErrorContains(t, err, "must be at least") + }) +} diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl index f652d56e09bc6..e5907da8e81ab 100644 --- a/templates/shared/actions/runner_list.tmpl +++ b/templates/shared/actions/runner_list.tmpl @@ -3,7 +3,7 @@

{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
-
From 02c64e48b7421e68d264163253318aacb6fb2f3d Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 24 Dec 2024 00:31:53 +0000 Subject: [PATCH 4/5] [skip ci] Updated translations via Crowdin --- options/locale/locale_cs-CZ.ini | 1 + options/locale/locale_de-DE.ini | 1 + options/locale/locale_el-GR.ini | 1 + options/locale/locale_es-ES.ini | 1 + options/locale/locale_fa-IR.ini | 1 + options/locale/locale_fi-FI.ini | 1 + options/locale/locale_fr-FR.ini | 3 +++ options/locale/locale_ga-IE.ini | 1 + options/locale/locale_hu-HU.ini | 1 + options/locale/locale_id-ID.ini | 1 + options/locale/locale_is-IS.ini | 1 + options/locale/locale_it-IT.ini | 1 + options/locale/locale_ja-JP.ini | 1 + options/locale/locale_ko-KR.ini | 1 + options/locale/locale_lv-LV.ini | 1 + options/locale/locale_nl-NL.ini | 1 + options/locale/locale_pl-PL.ini | 1 + options/locale/locale_pt-BR.ini | 1 + options/locale/locale_pt-PT.ini | 3 +++ options/locale/locale_ru-RU.ini | 1 + options/locale/locale_si-LK.ini | 1 + options/locale/locale_sk-SK.ini | 1 + options/locale/locale_sv-SE.ini | 1 + options/locale/locale_tr-TR.ini | 1 + options/locale/locale_uk-UA.ini | 1 + options/locale/locale_zh-CN.ini | 3 +++ options/locale/locale_zh-HK.ini | 1 + options/locale/locale_zh-TW.ini | 1 + 28 files changed, 34 insertions(+) diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 2abe3672cd834..3c7e247d1318b 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -3742,6 +3742,7 @@ variables.creation.success=Proměnná „%s“ byla přidána. variables.update.failed=Úprava proměnné se nezdařila. variables.update.success=Proměnná byla upravena. + [projects] deleted.display_name=Odstraněný projekt type-1.display_name=Samostatný projekt diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index c8dd3f71a23ba..78bfb26d73aad 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -3556,6 +3556,7 @@ variables.creation.success=Die Variable „%s“ wurde hinzugefügt. variables.update.failed=Fehler beim Bearbeiten der Variable. variables.update.success=Die Variable wurde bearbeitet. + [projects] type-1.display_name=Individuelles Projekt type-2.display_name=Repository-Projekt diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 193441828ae1e..db7b3ffae31d6 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -3439,6 +3439,7 @@ variables.creation.success=Η μεταβλητή "%s" έχει προστεθε variables.update.failed=Αποτυχία επεξεργασίας μεταβλητής. variables.update.success=Η μεταβλητή έχει τροποποιηθεί. + [projects] type-1.display_name=Ατομικό Έργο type-2.display_name=Έργο Αποθετηρίου diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index e95513766bb3b..010bee186d804 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -3415,6 +3415,7 @@ variables.creation.success=La variable "%s" ha sido añadida. variables.update.failed=Error al editar la variable. variables.update.success=La variable ha sido editada. + [projects] type-1.display_name=Proyecto individual type-2.display_name=Proyecto repositorio diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 640592f2bfbb3..b92361db34b83 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -2529,6 +2529,7 @@ runs.commit=کامیت + [projects] [git.filemode] diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 375c7b11bf928..b4f3869db41eb 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -1707,6 +1707,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index d522abbd11c21..0898d0fd29f59 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1945,6 +1945,8 @@ pulls.delete.title=Supprimer cette demande d'ajout ? pulls.delete.text=Voulez-vous vraiment supprimer cet demande d'ajout ? (Cela supprimera définitivement tout le contenu. Envisagez de le fermer à la place, si vous avez l'intention de le garder archivé) pulls.recently_pushed_new_branches=Vous avez soumis sur la branche %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Cette branche est en retard de %d révision sur %s +pulls.upstream_diverging_prompt_behind_n=Cette branche est en retard de %d révisions sur %s pulls.upstream_diverging_prompt_base_newer=La branche de base %s a de nouveaux changements pulls.upstream_diverging_merge=Synchroniser la bifurcation @@ -3770,6 +3772,7 @@ variables.creation.success=La variable « %s » a été ajoutée. variables.update.failed=Impossible d’éditer la variable. variables.update.success=La variable a bien été modifiée. + [projects] deleted.display_name=Projet supprimé type-1.display_name=Projet personnel diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 6d989d334798f..20672569ac0be 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -3770,6 +3770,7 @@ variables.creation.success=Tá an athróg "%s" curtha leis. variables.update.failed=Theip ar athróg a chur in eagar. variables.update.success=Tá an t-athróg curtha in eagar. + [projects] deleted.display_name=Tionscadal scriosta type-1.display_name=Tionscadal Aonair diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 88ccc9fac217b..8e11f07d33ab4 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -1615,6 +1615,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 00a406aae4510..7c8271b98b403 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -1444,6 +1444,7 @@ variables.creation.success=Variabel "%s" telah ditambahkan. variables.update.failed=Gagal mengedit variabel. variables.update.success=Variabel telah diedit. + [projects] type-1.display_name=Proyek Individu type-2.display_name=Proyek Repositori diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 0564d49b1c0b5..d3824f83a2146 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -1342,6 +1342,7 @@ runs.commit=Framlag + [projects] [git.filemode] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 567e6acdcefc5..26b4f9e27b481 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -2807,6 +2807,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index bbd48989f7bed..da07d77825c6c 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -3762,6 +3762,7 @@ variables.creation.success=変数 "%s" を追加しました。 variables.update.failed=変数を更新できませんでした。 variables.update.success=変数を更新しました。 + [projects] deleted.display_name=削除されたプロジェクト type-1.display_name=個人プロジェクト diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 48220d5c998eb..361ab23e4cd81 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -1563,6 +1563,7 @@ runs.commit=커밋 + [projects] [git.filemode] diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index fd412b95b416b..27a061d7224ed 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -3443,6 +3443,7 @@ variables.creation.success=Mainīgais "%s" tika pievienots. variables.update.failed=Neizdevās labot mainīgo. variables.update.success=Mainīgais tika labots. + [projects] type-1.display_name=Individuālais projekts type-2.display_name=Repozitorija projekts diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index a4da8177bce30..5c07b521e9b04 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -2537,6 +2537,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 22f701219d3c3..13c05eebe079b 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -2430,6 +2430,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 8fb869898b56c..c053b33210049 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -3353,6 +3353,7 @@ runs.empty_commit_message=(mensagem de commit vazia) need_approval_desc=Precisa de aprovação para executar workflows para pull request do fork. + [projects] type-1.display_name=Projeto individual type-2.display_name=Projeto do repositório diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 4db7b5a2a2b19..13e7318c6f218 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -3770,6 +3770,9 @@ variables.creation.success=A variável "%s" foi adicionada. variables.update.failed=Falha ao editar a variável. variables.update.success=A variável foi editada. +logs.always_auto_scroll=Rolar registos de forma automática e permanente +logs.always_expand_running=Expandir sempre os registos que vão rolando + [projects] deleted.display_name=Planeamento eliminado type-1.display_name=Planeamento individual diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 735077bd41407..0662c1f5f9b44 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -3373,6 +3373,7 @@ variables.creation.success=Переменная «%s» добавлена. variables.update.failed=Не удалось изменить переменную. variables.update.success=Переменная изменена. + [projects] type-1.display_name=Индивидуальный проект type-2.display_name=Проект репозитория diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 506fa5b492575..debcf8e3e90e6 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -2470,6 +2470,7 @@ runs.commit=කැප + [projects] [git.filemode] diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index b4bf6fb552a8d..39c13c358efb5 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -1328,6 +1328,7 @@ runners.labels=Štítky + [projects] [git.filemode] diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index fc138381db7c1..2fd8277b76f1c 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -2005,6 +2005,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 9b7f2cb5c6d13..d17985dba5002 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -3633,6 +3633,7 @@ variables.creation.success=`"%s" değişkeni eklendi.` variables.update.failed=Değişken düzenlenemedi. variables.update.success=Değişken düzenlendi. + [projects] deleted.display_name=Silinmiş Proje type-1.display_name=Kişisel Proje diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index efefeeb436be7..541a5bd0b55a3 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -2538,6 +2538,7 @@ runs.commit=Коміт + [projects] [git.filemode] diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 60fb75df4f649..a1fc26528e75c 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1945,6 +1945,8 @@ pulls.delete.title=删除此合并请求? pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 %[1]s +pulls.upstream_diverging_prompt_behind_1=该分支落后于 %[2]s %[1]d 个提交 +pulls.upstream_diverging_prompt_behind_n=该分支落后于 %[2]s %[1]d 个提交 pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改 pulls.upstream_diverging_merge=同步派生 @@ -3770,6 +3772,7 @@ variables.creation.success=变量 “%s” 添加成功。 variables.update.failed=编辑变量失败。 variables.update.success=该变量已被编辑。 + [projects] deleted.display_name=已删除项目 type-1.display_name=个人项目 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 6f37d30efcf56..e1ca6ebfc83ca 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -975,6 +975,7 @@ runners.task_list.repository=儲存庫 + [projects] [git.filemode] diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 0bfc6d6365f1d..267f8b49475cd 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -3763,6 +3763,7 @@ variables.creation.success=已新增變數「%s」。 variables.update.failed=編輯變數失敗。 variables.update.success=已編輯變數。 + [projects] deleted.display_name=已刪除的專案 type-1.display_name=個人專案 From 781c6df40fcd8c8a112d048b4beb079d0b9a7f2b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 24 Dec 2024 09:54:19 +0800 Subject: [PATCH 5/5] Add sub issue list support (#32940) Just like GitHub, show issue icon/title when the issue number is in a list --- models/unittest/testdb.go | 2 +- modules/markup/html_commit.go | 19 ++++ modules/markup/html_issue.go | 90 +++++++++++-------- modules/markup/html_issue_test.go | 72 +++++++++++++++ modules/markup/render_helper.go | 1 + modules/references/references.go | 22 +++-- modules/references/references_test.go | 5 +- modules/svg/processor.go | 37 ++++---- routers/init.go | 2 +- services/context/context_response.go | 2 +- services/markup/main_test.go | 2 +- .../{processorhelper.go => renderhelper.go} | 3 +- ...preview.go => renderhelper_codepreview.go} | 3 +- ...st.go => renderhelper_codepreview_test.go} | 5 +- .../markup/renderhelper_issueicontitle.go | 66 ++++++++++++++ .../renderhelper_issueicontitle_test.go | 49 ++++++++++ ...r_test.go => renderhelper_mention_test.go} | 18 ++-- templates/shared/issueicon.tmpl | 48 +++++----- tests/test_utils.go | 2 +- 19 files changed, 332 insertions(+), 116 deletions(-) create mode 100644 modules/markup/html_issue_test.go rename services/markup/{processorhelper.go => renderhelper.go} (88%) rename services/markup/{processorhelper_codepreview.go => renderhelper_codepreview.go} (97%) rename services/markup/{processorhelper_codepreview_test.go => renderhelper_codepreview_test.go} (95%) create mode 100644 services/markup/renderhelper_issueicontitle.go create mode 100644 services/markup/renderhelper_issueicontitle_test.go rename services/markup/{processorhelper_test.go => renderhelper_mention_test.go} (61%) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 5794d5109ede4..12f3c25676d8c 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -206,7 +206,7 @@ func CreateTestEngine(opts FixturesOptions) error { x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") if err != nil { if strings.Contains(err.Error(), "unknown driver") { - return fmt.Errorf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) + return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) } return err } diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go index 358e7b06ba538..aa1b7d034a594 100644 --- a/modules/markup/html_commit.go +++ b/modules/markup/html_commit.go @@ -8,6 +8,7 @@ import ( "strings" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" @@ -194,3 +195,21 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling.NextSibling } } + +func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { + next := node.NextSibling + + for node != nil && node != next { + found, ref := references.FindRenderizableCommitCrossReference(node.Data) + if !found { + return + } + + reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) + linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) + link := createLink(ctx, linkHref, reftext, "commit") + + replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) + node = node.NextSibling.NextSibling + } +} diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go index e64ec76c3d2fc..7a6f33011a7c0 100644 --- a/modules/markup/html_issue.go +++ b/modules/markup/html_issue.go @@ -4,9 +4,9 @@ package markup import ( + "strconv" "strings" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" @@ -16,8 +16,16 @@ import ( "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" + "golang.org/x/net/html/atom" ) +type RenderIssueIconTitleOptions struct { + OwnerName string + RepoName string + LinkHref string + IssueIndex int64 +} + func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.RenderOptions.Metas == nil { return @@ -66,6 +74,27 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { } } +func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref *references.RenderizableReference) *html.Node { + if DefaultRenderHelperFuncs.RenderRepoIssueIconTitle == nil { + return nil + } + issueIndex, _ := strconv.ParseInt(ref.Issue, 10, 64) + h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{ + OwnerName: ref.Owner, + RepoName: ref.Name, + LinkHref: linkHref, + IssueIndex: issueIndex, + }) + if err != nil { + log.Error("RenderRepoIssueIconTitle failed: %v", err) + return nil + } + if h == "" { + return nil + } + return &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(h))} +} + func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.RenderOptions.Metas == nil { return @@ -76,32 +105,28 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { // old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true" - var ( - found bool - ref *references.RenderizableReference - ) + var ref *references.RenderizableReference next := node.NextSibling - for node != nil && node != next { _, hasExtTrackFormat := ctx.RenderOptions.Metas["format"] // Repos with external issue trackers might still need to reference local PRs // We need to concern with the first one that shows up in the text, whichever it is isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric - foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly) + refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly) switch ctx.RenderOptions.Metas["style"] { case "", IssueNameStyleNumeric: - found, ref = foundNumeric, refNumeric + ref = refNumeric case IssueNameStyleAlphanumeric: - found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data) + ref = references.FindRenderizableReferenceAlphanumeric(node.Data) case IssueNameStyleRegexp: pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"]) if err != nil { return } - found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern) + ref = references.FindRenderizableReferenceRegexp(node.Data, pattern) } // Repos with external issue trackers might still need to reference local PRs @@ -109,17 +134,17 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { if hasExtTrackFormat && !isNumericStyle && refNumeric != nil { // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that // Allow a free-pass when non-numeric pattern wasn't found. - if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) { - found = foundNumeric + if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start { ref = refNumeric } } - if !found { + + if ref == nil { return } var link *html.Node - reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] + refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End] if hasExtTrackFormat && !ref.IsPull { ctx.RenderOptions.Metas["index"] = ref.Issue @@ -129,18 +154,23 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err) } - link = createLink(ctx, res, reftext, "ref-issue ref-external-issue") + link = createLink(ctx, res, refText, "ref-issue ref-external-issue") } else { // Path determines the type of link that will be rendered. It's unknown at this point whether // the linked item is actually a PR or an issue. Luckily it's of no real consequence because // Gitea will redirect on click as appropriate. + issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner) + issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name) issuePath := util.Iif(ref.IsPull, "pulls", "issues") - if ref.Owner == "" { - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), LinkTypeApp) - link = createLink(ctx, linkHref, reftext, "ref-issue") - } else { - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp) - link = createLink(ctx, linkHref, reftext, "ref-issue") + linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp) + + // at the moment, only render the issue index in a full line (or simple line) as icon+title + // otherwise it would be too noisy for "take #1 as an example" in a sentence + if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) { + link = createIssueLinkContentWithSummary(ctx, linkHref, ref) + } + if link == nil { + link = createLink(ctx, linkHref, refText, "ref-issue") } } @@ -168,21 +198,3 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling.NextSibling.NextSibling.NextSibling } } - -func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { - next := node.NextSibling - - for node != nil && node != next { - found, ref := references.FindRenderizableCommitCrossReference(node.Data) - if !found { - return - } - - reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) - link := createLink(ctx, linkHref, reftext, "commit") - - replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) - node = node.NextSibling.NextSibling - } -} diff --git a/modules/markup/html_issue_test.go b/modules/markup/html_issue_test.go new file mode 100644 index 0000000000000..8d189fbdf62e0 --- /dev/null +++ b/modules/markup/html_issue_test.go @@ -0,0 +1,72 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup_test + +import ( + "context" + "html/template" + "strings" + "testing" + + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + testModule "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRender_IssueList(t *testing.T) { + defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + markup.Init(&markup.RenderHelperFuncs{ + RenderRepoIssueIconTitle: func(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (template.HTML, error) { + return htmlutil.HTMLFormat("
issue #%d
", opts.IssueIndex), nil + }, + }) + + test := func(input, expected string) { + rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{ + "user": "test-user", "repo": "test-repo", + "markupAllowShortIssuePattern": "true", + }) + out, err := markdown.RenderString(rctx, input) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out))) + } + + t.Run("NormalIssueRef", func(t *testing.T) { + test( + "#12345", + `

#12345

`, + ) + }) + + t.Run("ListIssueRef", func(t *testing.T) { + test( + "* #12345", + `
    +
  • issue #12345
  • +
`, + ) + }) + + t.Run("ListIssueRefNormal", func(t *testing.T) { + test( + "* foo #12345 bar", + ``, + ) + }) + + t.Run("ListTodoIssueRef", func(t *testing.T) { + test( + "* [ ] #12345", + `
    +
  • issue #12345
  • +
`, + ) + }) +} diff --git a/modules/markup/render_helper.go b/modules/markup/render_helper.go index 82796ef274558..8ff0e7d6fb41c 100644 --- a/modules/markup/render_helper.go +++ b/modules/markup/render_helper.go @@ -38,6 +38,7 @@ type RenderHelper interface { type RenderHelperFuncs struct { IsUsernameMentionable func(ctx context.Context, username string) bool RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error) + RenderRepoIssueIconTitle func(ctx context.Context, options RenderIssueIconTitleOptions) (template.HTML, error) } var DefaultRenderHelperFuncs *RenderHelperFuncs diff --git a/modules/references/references.go b/modules/references/references.go index dcb70a33d0f6d..a5b102b7f2e48 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -330,22 +330,22 @@ func FindAllIssueReferences(content string) []IssueReference { } // FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string. -func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) (bool, *RenderizableReference) { +func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) *RenderizableReference { var match []int if !crossLinkOnly { match = issueNumericPattern.FindStringSubmatchIndex(content) } if match == nil { if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil { - return false, nil + return nil } } r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly) if r == nil { - return false, nil + return nil } - return true, &RenderizableReference{ + return &RenderizableReference{ Issue: r.issue, Owner: r.owner, Name: r.name, @@ -372,15 +372,14 @@ func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableRe } // FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string. -func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bool, *RenderizableReference) { +func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *RenderizableReference { match := pattern.FindStringSubmatchIndex(content) if len(match) < 4 { - return false, nil + return nil } action, location := findActionKeywords([]byte(content), match[2]) - - return true, &RenderizableReference{ + return &RenderizableReference{ Issue: content[match[2]:match[3]], RefLocation: &RefSpan{Start: match[0], End: match[1]}, Action: action, @@ -390,15 +389,14 @@ func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bo } // FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string. -func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) { +func FindRenderizableReferenceAlphanumeric(content string) *RenderizableReference { match := issueAlphanumericPattern.FindStringSubmatchIndex(content) if match == nil { - return false, nil + return nil } action, location := findActionKeywords([]byte(content), match[2]) - - return true, &RenderizableReference{ + return &RenderizableReference{ Issue: content[match[2]:match[3]], RefLocation: &RefSpan{Start: match[2], End: match[3]}, Action: action, diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 27803083c0b74..1b6a968d6a190 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -249,11 +249,10 @@ func TestFindAllIssueReferences(t *testing.T) { } for _, fixture := range alnumFixtures { - found, ref := FindRenderizableReferenceAlphanumeric(fixture.input) + ref := FindRenderizableReferenceAlphanumeric(fixture.input) if fixture.issue == "" { - assert.False(t, found, "Failed to parse: {%s}", fixture.input) + assert.Nil(t, ref, "Failed to parse: {%s}", fixture.input) } else { - assert.True(t, found, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.issue, ref.Issue, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.refLocation, ref.RefLocation, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input) diff --git a/modules/svg/processor.go b/modules/svg/processor.go index 82248fb0c1216..4fcb11a57d0c2 100644 --- a/modules/svg/processor.go +++ b/modules/svg/processor.go @@ -10,7 +10,7 @@ import ( "sync" ) -type normalizeVarsStruct struct { +type globalVarsStruct struct { reXMLDoc, reComment, reAttrXMLNs, @@ -18,26 +18,23 @@ type normalizeVarsStruct struct { reAttrClassPrefix *regexp.Regexp } -var ( - normalizeVars *normalizeVarsStruct - normalizeVarsOnce sync.Once -) +var globalVars = sync.OnceValue(func() *globalVarsStruct { + return &globalVarsStruct{ + reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), + reComment: regexp.MustCompile(`(?s)`), + + reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), + reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), + reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), + } +}) // Normalize normalizes the SVG content: set default width/height, remove unnecessary tags/attributes // It's designed to work with valid SVG content. For invalid SVG content, the returned content is not guaranteed. func Normalize(data []byte, size int) []byte { - normalizeVarsOnce.Do(func() { - normalizeVars = &normalizeVarsStruct{ - reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), - reComment: regexp.MustCompile(`(?s)`), - - reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), - reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), - reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), - } - }) - data = normalizeVars.reXMLDoc.ReplaceAll(data, nil) - data = normalizeVars.reComment.ReplaceAll(data, nil) + vars := globalVars() + data = vars.reXMLDoc.ReplaceAll(data, nil) + data = vars.reComment.ReplaceAll(data, nil) data = bytes.TrimSpace(data) svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">")) @@ -45,9 +42,9 @@ func Normalize(data []byte, size int) []byte { return data } normalized := bytes.Clone(svgTag) - normalized = normalizeVars.reAttrXMLNs.ReplaceAll(normalized, nil) - normalized = normalizeVars.reAttrSize.ReplaceAll(normalized, nil) - normalized = normalizeVars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`)) + normalized = vars.reAttrXMLNs.ReplaceAll(normalized, nil) + normalized = vars.reAttrSize.ReplaceAll(normalized, nil) + normalized = vars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`)) normalized = bytes.TrimSpace(normalized) normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size) if !bytes.Contains(normalized, []byte(` class="`)) { diff --git a/routers/init.go b/routers/init.go index 98ce1bc4c9858..e7aa765bf03c9 100644 --- a/routers/init.go +++ b/routers/init.go @@ -133,7 +133,7 @@ func InitWebInstalled(ctx context.Context) { highlight.NewContext() external.RegisterRenderers() - markup.Init(markup_service.ProcessorHelper()) + markup.Init(markup_service.FormalRenderHelperFuncs()) if setting.EnableSQLite3 { log.Info("SQLite3 support is enabled") diff --git a/services/context/context_response.go b/services/context/context_response.go index 4c086ea9f572b..c7044791eb6e2 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -106,7 +106,7 @@ func (ctx *Context) JSONTemplate(tmpl templates.TplName) { } // RenderToHTML renders the template content to a HTML string -func (ctx *Context) RenderToHTML(name templates.TplName, data map[string]any) (template.HTML, error) { +func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTML, error) { var buf strings.Builder err := ctx.Render.HTML(&buf, 0, name, data, ctx.TemplateContext) return template.HTML(buf.String()), err diff --git a/services/markup/main_test.go b/services/markup/main_test.go index 5553ebc058948..d04a18bfa17e9 100644 --- a/services/markup/main_test.go +++ b/services/markup/main_test.go @@ -11,6 +11,6 @@ import ( func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ - FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"}, + FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml", "issue.yml"}, }) } diff --git a/services/markup/processorhelper.go b/services/markup/renderhelper.go similarity index 88% rename from services/markup/processorhelper.go rename to services/markup/renderhelper.go index 1f1abf496a3e0..4b9852b48bf2f 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/renderhelper.go @@ -11,9 +11,10 @@ import ( gitea_context "code.gitea.io/gitea/services/context" ) -func ProcessorHelper() *markup.RenderHelperFuncs { +func FormalRenderHelperFuncs() *markup.RenderHelperFuncs { return &markup.RenderHelperFuncs{ RenderRepoFileCodePreview: renderRepoFileCodePreview, + RenderRepoIssueIconTitle: renderRepoIssueIconTitle, IsUsernameMentionable: func(ctx context.Context, username string) bool { mentionedUser, err := user.GetUserByName(ctx, username) if err != nil { diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/renderhelper_codepreview.go similarity index 97% rename from services/markup/processorhelper_codepreview.go rename to services/markup/renderhelper_codepreview.go index 0500e57e4610e..170c70c4098e3 100644 --- a/services/markup/processorhelper_codepreview.go +++ b/services/markup/renderhelper_codepreview.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" gitea_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/repository/files" ) @@ -46,7 +47,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie return "", err } if !perms.CanRead(unit.TypeCode) { - return "", fmt.Errorf("no permission") + return "", util.ErrPermissionDenied } gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo) diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/renderhelper_codepreview_test.go similarity index 95% rename from services/markup/processorhelper_codepreview_test.go rename to services/markup/renderhelper_codepreview_test.go index 154e4e8e44118..ea945584b427e 100644 --- a/services/markup/processorhelper_codepreview_test.go +++ b/services/markup/renderhelper_codepreview_test.go @@ -9,12 +9,13 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" ) -func TestProcessorHelperCodePreview(t *testing.T) { +func TestRenderHelperCodePreview(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) @@ -79,5 +80,5 @@ func TestProcessorHelperCodePreview(t *testing.T) { LineStart: 1, LineStop: 10, }) - assert.ErrorContains(t, err, "no permission") + assert.ErrorIs(t, err, util.ErrPermissionDenied) } diff --git a/services/markup/renderhelper_issueicontitle.go b/services/markup/renderhelper_issueicontitle.go new file mode 100644 index 0000000000000..53a508e908145 --- /dev/null +++ b/services/markup/renderhelper_issueicontitle.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup + +import ( + "context" + "fmt" + "html/template" + + "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/util" + gitea_context "code.gitea.io/gitea/services/context" +) + +func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (_ template.HTML, err error) { + webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context) + if !ok { + return "", fmt.Errorf("context is not a web context") + } + + textIssueIndex := fmt.Sprintf("(#%d)", opts.IssueIndex) + dbRepo := webCtx.Repo.Repository + if opts.OwnerName != "" { + dbRepo, err = repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName) + if err != nil { + return "", err + } + textIssueIndex = fmt.Sprintf("(%s/%s#%d)", dbRepo.OwnerName, dbRepo.Name, opts.IssueIndex) + } + if dbRepo == nil { + return "", nil + } + + issue, err := issues.GetIssueByIndex(ctx, dbRepo.ID, opts.IssueIndex) + if err != nil { + return "", err + } + + if webCtx.Repo.Repository == nil || dbRepo.ID != webCtx.Repo.Repository.ID { + perms, err := access.GetUserRepoPermission(ctx, dbRepo, webCtx.Doer) + if err != nil { + return "", err + } + if !perms.CanReadIssuesOrPulls(issue.IsPull) { + return "", util.ErrPermissionDenied + } + } + + if issue.IsPull { + if err = issue.LoadPullRequest(ctx); err != nil { + return "", err + } + } + + htmlIcon, err := webCtx.RenderToHTML("shared/issueicon", issue) + if err != nil { + return "", err + } + + return htmlutil.HTMLFormat(`%s %s %s`, opts.LinkHref, htmlIcon, issue.Title, textIssueIndex), nil +} diff --git a/services/markup/renderhelper_issueicontitle_test.go b/services/markup/renderhelper_issueicontitle_test.go new file mode 100644 index 0000000000000..adce8401e076b --- /dev/null +++ b/services/markup/renderhelper_issueicontitle_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup + +import ( + "testing" + + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestRenderHelperIssueIconTitle(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + htm, err := renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ + LinkHref: "/link", + IssueIndex: 1, + }) + assert.NoError(t, err) + assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (#1)`, string(htm)) + + ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + htm, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ + OwnerName: "user2", + RepoName: "repo1", + LinkHref: "/link", + IssueIndex: 1, + }) + assert.NoError(t, err) + assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (user2/repo1#1)`, string(htm)) + + ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + _, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ + OwnerName: "user2", + RepoName: "repo2", + LinkHref: "/link", + IssueIndex: 2, + }) + assert.ErrorIs(t, err, util.ErrPermissionDenied) +} diff --git a/services/markup/processorhelper_test.go b/services/markup/renderhelper_mention_test.go similarity index 61% rename from services/markup/processorhelper_test.go rename to services/markup/renderhelper_mention_test.go index 170edae0e0beb..f0c0eb9926ad5 100644 --- a/services/markup/processorhelper_test.go +++ b/services/markup/renderhelper_mention_test.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestProcessorHelper(t *testing.T) { +func TestRenderHelperMention(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) userPublic := "user1" @@ -32,10 +32,10 @@ func TestProcessorHelper(t *testing.T) { unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0) // when using general context, use user's visibility to check - assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPublic)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userLimited)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPrivate)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userNoSuch)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPublic)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userLimited)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPrivate)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userNoSuch)) // when using web context, use user.IsUserVisibleToViewer to check req, err := http.NewRequest("GET", "/", nil) @@ -44,11 +44,11 @@ func TestProcessorHelper(t *testing.T) { defer baseCleanUp() giteaCtx := gitea_context.NewWebContext(base, &contexttest.MockRender{}, nil) - assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate)) giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate) assert.NoError(t, err) - assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) - assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate)) } diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl index a62714e988ca3..f828de5c6694e 100644 --- a/templates/shared/issueicon.tmpl +++ b/templates/shared/issueicon.tmpl @@ -1,25 +1,25 @@ -{{if .IsPull}} - {{if not .PullRequest}} +{{- if .IsPull -}} + {{- if not .PullRequest -}} No PullRequest - {{else}} - {{if .IsClosed}} - {{if .PullRequest.HasMerged}} - {{svg "octicon-git-merge" 16 "text purple"}} - {{else}} - {{svg "octicon-git-pull-request" 16 "text red"}} - {{end}} - {{else}} - {{if .PullRequest.IsWorkInProgress ctx}} - {{svg "octicon-git-pull-request-draft" 16 "text grey"}} - {{else}} - {{svg "octicon-git-pull-request" 16 "text green"}} - {{end}} - {{end}} - {{end}} -{{else}} - {{if .IsClosed}} - {{svg "octicon-issue-closed" 16 "text red"}} - {{else}} - {{svg "octicon-issue-opened" 16 "text green"}} - {{end}} -{{end}} + {{- else -}} + {{- if .IsClosed -}} + {{- if .PullRequest.HasMerged -}} + {{- svg "octicon-git-merge" 16 "text purple" -}} + {{- else -}} + {{- svg "octicon-git-pull-request" 16 "text red" -}} + {{- end -}} + {{- else -}} + {{- if .PullRequest.IsWorkInProgress ctx -}} + {{- svg "octicon-git-pull-request-draft" 16 "text grey" -}} + {{- else -}} + {{- svg "octicon-git-pull-request" 16 "text green" -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- else -}} + {{- if .IsClosed -}} + {{- svg "octicon-issue-closed" 16 "text red" -}} + {{- else -}} + {{- svg "octicon-issue-opened" 16 "text green" -}} + {{- end -}} +{{- end -}} diff --git a/tests/test_utils.go b/tests/test_utils.go index 0fe0200ea7fac..96eb5731b4ac1 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -58,7 +58,7 @@ func InitTest(requireGitea bool) { _ = os.Setenv("GITEA_CONF", giteaConf) fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf) if !setting.EnableSQLite3 { - testlogger.Fatalf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify` + "\n") + testlogger.Fatalf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify` + "\n") } } if !filepath.IsAbs(giteaConf) {