diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index 33cbc507d9..c34066d318 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -15,7 +15,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - run: make generate-license generate-gitignore + - run: make generate-gitignore timeout-minutes: 40 - name: push translations to repo uses: appleboy/git-push-action@v0.0.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index c32915c1dc..7541bccb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ This changelog goes through the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.com). +## [1.23.5](https://github.com/go-gitea/gitea/releases/tag/v1.23.5) - 2025-03-04 + +* SECURITY + * Bump x/oauth2 & x/crypto (#33704) (#33727) +* PERFORMANCE + * Optimize user dashboard loading (#33686) (#33708) +* BUGFIXES + * Fix navbar dropdown item align (#33782) + * Fix inconsistent closed issue list icon (#33722) (#33728) + * Fix for Maven Package Naming Convention Handling (#33678) (#33679) + * Improve Open-with URL encoding (#33666) (#33680) + * Deleting repository should unlink all related packages (#33653) (#33673) + * Fix omitempty bug (#33663) (#33670) + * Upgrade go-crypto from 1.1.4 to 1.1.6 (#33745) (#33754) + * Fix OCI image.version annotation for releases to use full semver (#33698) (#33701) + * Try to fix ACME path when renew (#33668) (#33693) + * Fix mCaptcha bug (#33659) (#33661) + * Git graph: don't show detached commits (#33645) (#33650) + * Use MatchPhraseQuery for bleve code search (#33628) + * Adjust appearence of commit status webhook (#33778) #33789 + * Upgrade golang net from 0.35.0 -> 0.36.0 (#33795) #33796 + ## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16 * SECURITY diff --git a/Makefile b/Makefile index 0cc9269771..f7438c28b4 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ EXTRA_GOFLAGS ?= MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1) MAKE_EVIDENCE_DIR := .make_evidence -GOTESTFLAGS ?= -vet=off +GOTESTFLAGS ?= ifeq ($(RACE_ENABLED),true) GOFLAGS += -race GOTESTFLAGS += -race @@ -906,10 +906,6 @@ update-translations: mv ./translations/*.ini ./options/locale/ rmdir ./translations -.PHONY: generate-license -generate-license: ## update license files - $(GO) run build/generate-licenses.go - .PHONY: generate-gitignore generate-gitignore: ## update gitignore files $(GO) run build/generate-gitignores.go diff --git a/build/generate-licenses.go b/build/generate-licenses.go deleted file mode 100644 index 66e1d37755..0000000000 --- a/build/generate-licenses.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build ignore - -package main - -import ( - "archive/tar" - "compress/gzip" - "crypto/md5" - "encoding/hex" - "flag" - "fmt" - "io" - "log" - "net/http" - "os" - "path" - "path/filepath" - "strings" - - "code.gitea.io/gitea/build/license" - "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/util" -) - -func main() { - var ( - prefix = "gitea-licenses" - url = "https://api.github.com/repos/spdx/license-list-data/tarball" - githubApiToken = "" - githubUsername = "" - destination = "" - ) - - flag.StringVar(&destination, "dest", "options/license/", "destination for the licenses") - flag.StringVar(&githubUsername, "username", "", "github username") - flag.StringVar(&githubApiToken, "token", "", "github api token") - flag.Parse() - - file, err := os.CreateTemp(os.TempDir(), prefix) - if err != nil { - log.Fatalf("Failed to create temp file. %s", err) - } - - defer util.Remove(file.Name()) - - if err := os.RemoveAll(destination); err != nil { - log.Fatalf("Cannot clean destination folder: %v", err) - } - - if err := os.MkdirAll(destination, 0o755); err != nil { - log.Fatalf("Cannot create destination: %v", err) - } - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Fatalf("Failed to download archive. %s", err) - } - - if len(githubApiToken) > 0 && len(githubUsername) > 0 { - req.SetBasicAuth(githubUsername, githubApiToken) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - log.Fatalf("Failed to download archive. %s", err) - } - - defer resp.Body.Close() - - if _, err := io.Copy(file, resp.Body); err != nil { - log.Fatalf("Failed to copy archive to file. %s", err) - } - - if _, err := file.Seek(0, 0); err != nil { - log.Fatalf("Failed to reset seek on archive. %s", err) - } - - gz, err := gzip.NewReader(file) - if err != nil { - log.Fatalf("Failed to gunzip the archive. %s", err) - } - - tr := tar.NewReader(gz) - aliasesFiles := make(map[string][]string) - for { - hdr, err := tr.Next() - - if err == io.EOF { - break - } - - if err != nil { - log.Fatalf("Failed to iterate archive. %s", err) - } - - if !strings.Contains(hdr.Name, "/text/") { - continue - } - - if filepath.Ext(hdr.Name) != ".txt" { - continue - } - - fileBaseName := filepath.Base(hdr.Name) - licenseName := strings.TrimSuffix(fileBaseName, ".txt") - - if strings.HasPrefix(fileBaseName, "README") { - continue - } - - if strings.HasPrefix(fileBaseName, "deprecated_") { - continue - } - out, err := os.Create(path.Join(destination, licenseName)) - if err != nil { - log.Fatalf("Failed to create new file. %s", err) - } - - defer out.Close() - - // some license files have same content, so we need to detect these files and create a convert map into a json file - // Later we use this convert map to avoid adding same license content with different license name - h := md5.New() - // calculate md5 and write file in the same time - r := io.TeeReader(tr, h) - if _, err := io.Copy(out, r); err != nil { - log.Fatalf("Failed to write new file. %s", err) - } else { - fmt.Printf("Written %s\n", out.Name()) - - md5 := hex.EncodeToString(h.Sum(nil)) - aliasesFiles[md5] = append(aliasesFiles[md5], licenseName) - } - } - - // generate convert license name map - licenseAliases := make(map[string]string) - for _, fileNames := range aliasesFiles { - if len(fileNames) > 1 { - licenseName := license.GetLicenseNameFromAliases(fileNames) - if licenseName == "" { - // license name should not be empty as expected - // if it is empty, we need to rewrite the logic of GetLicenseNameFromAliases - log.Fatalf("GetLicenseNameFromAliases: license name is empty") - } - for _, fileName := range fileNames { - licenseAliases[fileName] = licenseName - } - } - } - // save convert license name map to file - b, err := json.Marshal(licenseAliases) - if err != nil { - log.Fatalf("Failed to create json bytes. %s", err) - } - - licenseAliasesDestination := filepath.Join(destination, "etc", "license-aliases.json") - if err := os.MkdirAll(filepath.Dir(licenseAliasesDestination), 0o755); err != nil { - log.Fatalf("Failed to create directory for license aliases json file. %s", err) - } - - f, err := os.Create(licenseAliasesDestination) - if err != nil { - log.Fatalf("Failed to create license aliases json file. %s", err) - } - defer f.Close() - - if _, err = f.Write(b); err != nil { - log.Fatalf("Failed to write license aliases json file. %s", err) - } - - fmt.Println("Done") -} diff --git a/build/license/aliasgenerator.go b/build/license/aliasgenerator.go deleted file mode 100644 index 7de1e6fbd6..0000000000 --- a/build/license/aliasgenerator.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package license - -import "strings" - -func GetLicenseNameFromAliases(fnl []string) string { - if len(fnl) == 0 { - return "" - } - - shortestItem := func(list []string) string { - s := list[0] - for _, l := range list[1:] { - if len(l) < len(s) { - s = l - } - } - return s - } - allHasPrefix := func(list []string, s string) bool { - for _, l := range list { - if !strings.HasPrefix(l, s) { - return false - } - } - return true - } - - sl := shortestItem(fnl) - slv := strings.Split(sl, "-") - var result string - for i := len(slv); i >= 0; i-- { - result = strings.Join(slv[:i], "-") - if allHasPrefix(fnl, result) { - return result - } - } - return "" -} diff --git a/build/license/aliasgenerator_test.go b/build/license/aliasgenerator_test.go deleted file mode 100644 index 239181b736..0000000000 --- a/build/license/aliasgenerator_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package license - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetLicenseNameFromAliases(t *testing.T) { - tests := []struct { - target string - inputs []string - }{ - { - // real case which you can find in license-aliases.json - target: "AGPL-1.0", - inputs: []string{ - "AGPL-1.0-only", - "AGPL-1.0-or-late", - }, - }, - { - target: "", - inputs: []string{ - "APSL-1.0", - "AGPL-1.0-only", - "AGPL-1.0-or-late", - }, - }, - } - - for _, tt := range tests { - result := GetLicenseNameFromAliases(tt.inputs) - assert.Equal(t, result, tt.target) - } -} diff --git a/cmd/doctor.go b/cmd/doctor.go index e433f4adc5..52699cc4dd 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "fmt" golog "log" "os" @@ -130,8 +131,8 @@ func runRecreateTable(ctx *cli.Context) error { } recreateTables := migrate_base.RecreateTables(beans...) - return db.InitEngineWithMigration(stdCtx, func(x *xorm.Engine) error { - if err := migrations.EnsureUpToDate(x); err != nil { + return db.InitEngineWithMigration(stdCtx, func(ctx context.Context, x *xorm.Engine) error { + if err := migrations.EnsureUpToDate(ctx, x); err != nil { return err } return recreateTables(x) diff --git a/cmd/migrate.go b/cmd/migrate.go index 459805a76d..25d8b50c45 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -7,9 +7,9 @@ import ( "context" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/migrations" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/versioned_migration" "github.com/urfave/cli/v2" ) @@ -36,7 +36,7 @@ func runMigrate(ctx *cli.Context) error { log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) - if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil { + if err := db.InitEngineWithMigration(context.Background(), versioned_migration.Migrate); err != nil { log.Fatal("Failed to initialize ORM engine: %v", err) return err } diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index 2e3aba021d..f9ed140395 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -13,7 +13,6 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/migrations" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -21,6 +20,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/services/versioned_migration" "github.com/urfave/cli/v2" ) @@ -227,7 +227,7 @@ func runMigrateStorage(ctx *cli.Context) error { log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) - if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil { + if err := db.InitEngineWithMigration(context.Background(), versioned_migration.Migrate); err != nil { log.Fatal("Failed to initialize ORM engine: %v", err) return err } diff --git a/cmd/web_acme.go b/cmd/web_acme.go index bca4ae0212..172dde913b 100644 --- a/cmd/web_acme.go +++ b/cmd/web_acme.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "github.com/caddyserver/certmagic" ) @@ -68,9 +69,15 @@ func runACME(listenAddr string, m http.Handler) error { // And one more thing, no idea why we should set the global default variables here // But it seems that the current ACME code needs these global variables to make renew work. // Otherwise, "renew" will use incorrect storage path + oldDefaultACME := certmagic.DefaultACME certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory} certmagic.DefaultACME = certmagic.ACMEIssuer{ - CA: setting.AcmeURL, + // try to use the default values provided by DefaultACME + CA: util.IfZero(setting.AcmeURL, oldDefaultACME.CA), + TestCA: oldDefaultACME.TestCA, + Logger: oldDefaultACME.Logger, + HTTPProxy: oldDefaultACME.HTTPProxy, + TrustedRoots: certPool, Email: setting.AcmeEmail, Agreed: setting.AcmeTOS, diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 899209874f..0fc49accef 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1294,6 +1294,9 @@ LEVEL = Info ;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css" ;THEMES = ;; +;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future. +;FILE_ICON_THEME = material +;; ;; All available reactions users can choose on issues/prs and comments. ;; Values can be emoji alias (:smile:) or a unicode emoji. ;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png @@ -1767,6 +1770,9 @@ LEVEL = Info ;; ;; convert \r\n to \n for Sendmail ;SENDMAIL_CONVERT_CRLF = true +;; +;; convert links of attached images to inline images. Only for images hosted in this gitea instance. +;EMBED_ATTACHMENT_IMAGES = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/models/activities/action.go b/models/activities/action.go index 52dffe07fd..c16c49c0ac 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -16,9 +16,7 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -200,15 +198,13 @@ func (a *Action) LoadActUser(ctx context.Context) { } } -func (a *Action) LoadRepo(ctx context.Context) { +func (a *Action) LoadRepo(ctx context.Context) error { if a.Repo != nil { - return + return nil } var err error a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID) - if err != nil { - log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err) - } + return err } // GetActFullName gets the action's user full name. @@ -250,7 +246,7 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string { // GetRepoUserName returns the name of the action repository owner. func (a *Action) GetRepoUserName(ctx context.Context) string { - a.LoadRepo(ctx) + _ = a.LoadRepo(ctx) if a.Repo == nil { return "(non-existing-repo)" } @@ -265,7 +261,7 @@ func (a *Action) ShortRepoUserName(ctx context.Context) string { // GetRepoName returns the name of the action repository. func (a *Action) GetRepoName(ctx context.Context) string { - a.LoadRepo(ctx) + _ = a.LoadRepo(ctx) if a.Repo == nil { return "(non-existing-repo)" } @@ -529,8 +525,8 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. } if opts.RequestedTeam != nil { - env := repo_model.AccessibleTeamReposEnv(ctx, organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam) - teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) + env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam) + teamRepoIDs, err := env.RepoIDs(ctx, 1, opts.RequestedUser.NumRepos) if err != nil { return nil, fmt.Errorf("GetTeamRepositories: %w", err) } @@ -567,130 +563,6 @@ func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) return err } -// NotifyWatchers creates batch of actions for every watcher. -// It could insert duplicate actions for a repository action, like this: -// * Original action: UserID=1 (the real actor), ActUserID=1 -// * Organization action: UserID=100 (the repo's org), ActUserID=1 -// * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1 -func NotifyWatchers(ctx context.Context, actions ...*Action) error { - var watchers []*repo_model.Watch - var repo *repo_model.Repository - var err error - var permCode []bool - var permIssue []bool - var permPR []bool - - e := db.GetEngine(ctx) - - for _, act := range actions { - repoChanged := repo == nil || repo.ID != act.RepoID - - if repoChanged { - // Add feeds for user self and all watchers. - watchers, err = repo_model.GetWatchers(ctx, act.RepoID) - if err != nil { - return fmt.Errorf("get watchers: %w", err) - } - } - - // Add feed for actioner. - act.UserID = act.ActUserID - if _, err = e.Insert(act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) - } - - if repoChanged { - act.LoadRepo(ctx) - repo = act.Repo - - // check repo owner exist. - if err := act.Repo.LoadOwner(ctx); err != nil { - return fmt.Errorf("can't get repo owner: %w", err) - } - } else if act.Repo == nil { - act.Repo = repo - } - - // Add feed for organization - if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID { - act.ID = 0 - act.UserID = act.Repo.Owner.ID - if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) - } - } - - if repoChanged { - permCode = make([]bool, len(watchers)) - permIssue = make([]bool, len(watchers)) - permPR = make([]bool, len(watchers)) - for i, watcher := range watchers { - user, err := user_model.GetUserByID(ctx, watcher.UserID) - if err != nil { - permCode[i] = false - permIssue[i] = false - permPR[i] = false - continue - } - perm, err := access_model.GetUserRepoPermission(ctx, repo, user) - if err != nil { - permCode[i] = false - permIssue[i] = false - permPR[i] = false - continue - } - permCode[i] = perm.CanRead(unit.TypeCode) - permIssue[i] = perm.CanRead(unit.TypeIssues) - permPR[i] = perm.CanRead(unit.TypePullRequests) - } - } - - for i, watcher := range watchers { - if act.ActUserID == watcher.UserID { - continue - } - act.ID = 0 - act.UserID = watcher.UserID - act.Repo.Units = nil - - switch act.OpType { - case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch: - if !permCode[i] { - continue - } - case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue: - if !permIssue[i] { - continue - } - case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest: - if !permPR[i] { - continue - } - } - - if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new action: %w", err) - } - } - } - return nil -} - -// NotifyWatchersActions creates batch of actions for every watcher. -func NotifyWatchersActions(ctx context.Context, acts []*Action) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - for _, act := range acts { - if err := NotifyWatchers(ctx, act); err != nil { - return err - } - } - return committer.Commit() -} - // DeleteIssueActions delete all actions related with issueID func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error { // delete actions assigned to this issue diff --git a/models/activities/action_test.go b/models/activities/action_test.go index 9cfe981656..ee2a225a3e 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -82,43 +82,6 @@ func TestActivityReadable(t *testing.T) { } } -func TestNotifyWatchers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - action := &activities_model.Action{ - ActUserID: 8, - RepoID: 1, - OpType: activities_model.ActionStarRepo, - } - assert.NoError(t, activities_model.NotifyWatchers(db.DefaultContext, action)) - - // One watchers are inactive, thus action is only created for user 8, 1, 4, 11 - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ActUserID: action.ActUserID, - UserID: 8, - RepoID: action.RepoID, - OpType: action.OpType, - }) - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ActUserID: action.ActUserID, - UserID: 1, - RepoID: action.RepoID, - OpType: action.OpType, - }) - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ActUserID: action.ActUserID, - UserID: 4, - RepoID: action.RepoID, - OpType: action.OpType, - }) - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ActUserID: action.ActUserID, - UserID: 11, - RepoID: action.RepoID, - OpType: action.OpType, - }) -} - func TestConsistencyUpdateAction(t *testing.T) { if !setting.Database.Type.IsSQLite3() { t.Skip("Test is only for SQLite database.") diff --git a/models/db/engine_hook.go b/models/db/engine_hook.go index 2c9fc09c99..8709a2c2a1 100644 --- a/models/db/engine_hook.go +++ b/models/db/engine_hook.go @@ -41,7 +41,7 @@ func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error { // 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function // is being displayed (the function that ultimately wants to execute the query in the code) // instead of the function of the slow query hook being called. - h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) + h.Logger.Log(8, &log.Event{Level: log.WARN}, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) } return nil } diff --git a/models/db/engine_init.go b/models/db/engine_init.go index edca697934..7a071fa29b 100644 --- a/models/db/engine_init.go +++ b/models/db/engine_init.go @@ -105,7 +105,7 @@ func UnsetDefaultEngine() { // When called from the "doctor" command, the migration function is a version check // that prevents the doctor from fixing anything in the database if the migration level // is different from the expected value. -func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) { +func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Context, *xorm.Engine) error) (err error) { if err = InitEngine(ctx); err != nil { return err } @@ -122,7 +122,7 @@ func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) // Installation should only be being re-run if users want to recover an old database. // However, we should think carefully about should we support re-install on an installed instance, // as there may be other problems due to secret reinitialization. - if err = migrateFunc(xormEngine); err != nil { + if err = migrateFunc(ctx, xormEngine); err != nil { return fmt.Errorf("migrate: %w", err) } diff --git a/models/db/log.go b/models/db/log.go index 307788ea2e..a9df6f541d 100644 --- a/models/db/log.go +++ b/models/db/log.go @@ -29,7 +29,7 @@ const stackLevel = 8 // Log a message with defined skip and at logging level func (l *XORMLogBridge) Log(skip int, level log.Level, format string, v ...any) { - l.logger.Log(skip+1, level, format, v...) + l.logger.Log(skip+1, &log.Event{Level: level}, format, v...) } // Debug show debug log diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index fd291c5692..5f79a873f1 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -413,7 +413,7 @@ func ExpectedDBVersion() int64 { } // EnsureUpToDate will check if the db is at the correct version -func EnsureUpToDate(x *xorm.Engine) error { +func EnsureUpToDate(ctx context.Context, x *xorm.Engine) error { currentDB, err := GetCurrentDBVersion(x) if err != nil { return err diff --git a/models/organization/org_test.go b/models/organization/org_test.go index b882a25be3..6638abd8e0 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -320,7 +320,7 @@ func TestAccessibleReposEnv_CountRepos(t *testing.T) { testSuccess := func(userID, expectedCount int64) { env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) - count, err := env.CountRepos() + count, err := env.CountRepos(db.DefaultContext) assert.NoError(t, err) assert.EqualValues(t, expectedCount, count) } @@ -334,7 +334,7 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) { testSuccess := func(userID int64, expectedRepoIDs []int64) { env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) - repoIDs, err := env.RepoIDs(1, 100) + repoIDs, err := env.RepoIDs(db.DefaultContext, 1, 100) assert.NoError(t, err) assert.Equal(t, expectedRepoIDs, repoIDs) } @@ -348,7 +348,7 @@ func TestAccessibleReposEnv_Repos(t *testing.T) { testSuccess := func(userID int64, expectedRepoIDs []int64) { env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) - repos, err := env.Repos(1, 100) + repos, err := env.Repos(db.DefaultContext, 1, 100) assert.NoError(t, err) expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs)) for i, repoID := range expectedRepoIDs { @@ -367,7 +367,7 @@ func TestAccessibleReposEnv_MirrorRepos(t *testing.T) { testSuccess := func(userID int64, expectedRepoIDs []int64) { env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) - repos, err := env.MirrorRepos() + repos, err := env.MirrorRepos(db.DefaultContext) assert.NoError(t, err) expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs)) for i, repoID := range expectedRepoIDs { diff --git a/models/repo/org_repo.go b/models/repo/org_repo.go index 5f0af2d475..fa519d25b1 100644 --- a/models/repo/org_repo.go +++ b/models/repo/org_repo.go @@ -47,10 +47,10 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo // AccessibleReposEnvironment operations involving the repositories that are // accessible to a particular user type AccessibleReposEnvironment interface { - CountRepos() (int64, error) - RepoIDs(page, pageSize int) ([]int64, error) - Repos(page, pageSize int) (RepositoryList, error) - MirrorRepos() (RepositoryList, error) + CountRepos(ctx context.Context) (int64, error) + RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) + Repos(ctx context.Context, page, pageSize int) (RepositoryList, error) + MirrorRepos(ctx context.Context) (RepositoryList, error) AddKeyword(keyword string) SetSort(db.SearchOrderBy) } @@ -60,7 +60,6 @@ type accessibleReposEnv struct { user *user_model.User team *org_model.Team teamIDs []int64 - ctx context.Context keyword string orderBy db.SearchOrderBy } @@ -86,18 +85,16 @@ func AccessibleReposEnv(ctx context.Context, org *org_model.Organization, userID org: org, user: user, teamIDs: teamIDs, - ctx: ctx, orderBy: db.SearchOrderByRecentUpdated, }, nil } // AccessibleTeamReposEnv an AccessibleReposEnvironment for the repositories in `org` // that are accessible to the specified team. -func AccessibleTeamReposEnv(ctx context.Context, org *org_model.Organization, team *org_model.Team) AccessibleReposEnvironment { +func AccessibleTeamReposEnv(org *org_model.Organization, team *org_model.Team) AccessibleReposEnvironment { return &accessibleReposEnv{ org: org, team: team, - ctx: ctx, orderBy: db.SearchOrderByRecentUpdated, } } @@ -123,8 +120,8 @@ func (env *accessibleReposEnv) cond() builder.Cond { return cond } -func (env *accessibleReposEnv) CountRepos() (int64, error) { - repoCount, err := db.GetEngine(env.ctx). +func (env *accessibleReposEnv) CountRepos(ctx context.Context) (int64, error) { + repoCount, err := db.GetEngine(ctx). Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). Where(env.cond()). Distinct("`repository`.id"). @@ -135,13 +132,13 @@ func (env *accessibleReposEnv) CountRepos() (int64, error) { return repoCount, nil } -func (env *accessibleReposEnv) RepoIDs(page, pageSize int) ([]int64, error) { +func (env *accessibleReposEnv) RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) { if page <= 0 { page = 1 } repoIDs := make([]int64, 0, pageSize) - return repoIDs, db.GetEngine(env.ctx). + return repoIDs, db.GetEngine(ctx). Table("repository"). Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). Where(env.cond()). @@ -152,8 +149,8 @@ func (env *accessibleReposEnv) RepoIDs(page, pageSize int) ([]int64, error) { Find(&repoIDs) } -func (env *accessibleReposEnv) Repos(page, pageSize int) (RepositoryList, error) { - repoIDs, err := env.RepoIDs(page, pageSize) +func (env *accessibleReposEnv) Repos(ctx context.Context, page, pageSize int) (RepositoryList, error) { + repoIDs, err := env.RepoIDs(ctx, page, pageSize) if err != nil { return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err) } @@ -163,15 +160,15 @@ func (env *accessibleReposEnv) Repos(page, pageSize int) (RepositoryList, error) return repos, nil } - return repos, db.GetEngine(env.ctx). + return repos, db.GetEngine(ctx). In("`repository`.id", repoIDs). OrderBy(string(env.orderBy)). Find(&repos) } -func (env *accessibleReposEnv) MirrorRepoIDs() ([]int64, error) { +func (env *accessibleReposEnv) MirrorRepoIDs(ctx context.Context) ([]int64, error) { repoIDs := make([]int64, 0, 10) - return repoIDs, db.GetEngine(env.ctx). + return repoIDs, db.GetEngine(ctx). Table("repository"). Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id AND `repository`.is_mirror=?", true). Where(env.cond()). @@ -181,8 +178,8 @@ func (env *accessibleReposEnv) MirrorRepoIDs() ([]int64, error) { Find(&repoIDs) } -func (env *accessibleReposEnv) MirrorRepos() (RepositoryList, error) { - repoIDs, err := env.MirrorRepoIDs() +func (env *accessibleReposEnv) MirrorRepos(ctx context.Context) (RepositoryList, error) { + repoIDs, err := env.MirrorRepoIDs(ctx) if err != nil { return nil, fmt.Errorf("MirrorRepoIDs: %w", err) } @@ -192,7 +189,7 @@ func (env *accessibleReposEnv) MirrorRepos() (RepositoryList, error) { return repos, nil } - return repos, db.GetEngine(env.ctx). + return repos, db.GetEngine(ctx). In("`repository`.id", repoIDs). Find(&repos) } diff --git a/modules/base/tool.go b/modules/base/tool.go index b6ed8cbf9a..02ca85569e 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -17,7 +17,6 @@ import ( "strings" "time" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -139,24 +138,3 @@ func Int64sToStrings(ints []int64) []string { } return strs } - -// EntryIcon returns the octicon name for displaying files/directories -func EntryIcon(entry *git.TreeEntry) string { - switch { - case entry.IsLink(): - te, err := entry.FollowLink() - if err != nil { - return "file-symlink-file" - } - if te.IsDir() { - return "file-directory-symlink" - } - return "file-symlink-file" - case entry.IsDir(): - return "file-directory-fill" - case entry.IsSubModule(): - return "file-submodule" - } - - return "file" -} diff --git a/modules/fileicon/basic.go b/modules/fileicon/basic.go new file mode 100644 index 0000000000..040a8e87de --- /dev/null +++ b/modules/fileicon/basic.go @@ -0,0 +1,27 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon + +import ( + "html/template" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/svg" +) + +func BasicThemeIcon(entry *git.TreeEntry) template.HTML { + svgName := "octicon-file" + switch { + case entry.IsLink(): + svgName = "octicon-file-symlink-file" + if te, err := entry.FollowLink(); err == nil && te.IsDir() { + svgName = "octicon-file-directory-symlink" + } + case entry.IsDir(): + svgName = "octicon-file-directory-fill" + case entry.IsSubModule(): + svgName = "octicon-file-submodule" + } + return svg.RenderHTML(svgName) +} diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go new file mode 100644 index 0000000000..201cf6f455 --- /dev/null +++ b/modules/fileicon/material.go @@ -0,0 +1,141 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon + +import ( + "html/template" + "path" + "strings" + "sync" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/options" + "code.gitea.io/gitea/modules/reqctx" + "code.gitea.io/gitea/modules/svg" +) + +type materialIconRulesData struct { + FileNames map[string]string `json:"fileNames"` + FolderNames map[string]string `json:"folderNames"` + FileExtensions map[string]string `json:"fileExtensions"` +} + +type MaterialIconProvider struct { + once sync.Once + rules *materialIconRulesData + svgs map[string]string +} + +var materialIconProvider MaterialIconProvider + +func DefaultMaterialIconProvider() *MaterialIconProvider { + materialIconProvider.once.Do(materialIconProvider.loadData) + return &materialIconProvider +} + +func (m *MaterialIconProvider) loadData() { + buf, err := options.AssetFS().ReadFile("fileicon/material-icon-rules.json") + if err != nil { + log.Error("Failed to read material icon rules: %v", err) + return + } + err = json.Unmarshal(buf, &m.rules) + if err != nil { + log.Error("Failed to unmarshal material icon rules: %v", err) + return + } + + buf, err = options.AssetFS().ReadFile("fileicon/material-icon-svgs.json") + if err != nil { + log.Error("Failed to read material icon rules: %v", err) + return + } + err = json.Unmarshal(buf, &m.svgs) + if err != nil { + log.Error("Failed to unmarshal material icon rules: %v", err) + return + } + log.Debug("Loaded material icon rules and SVG images") +} + +func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name, svg string) template.HTML { + data := ctx.GetData() + renderedSVGs, _ := data["_RenderedSVGs"].(map[string]bool) + if renderedSVGs == nil { + renderedSVGs = make(map[string]bool) + data["_RenderedSVGs"] = renderedSVGs + } + // This part is a bit hacky, but it works really well. It should be safe to do so because all SVG icons are generated by us. + // Will try to refactor this in the future. + if !strings.HasPrefix(svg, "