diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 22cb784245..0b23de0a66 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -154,12 +154,15 @@ jobs: runs-on: ubuntu-latest services: mysql: - image: mysql:8.0 + # the bitnami mysql image has more options than the official one, it's easier to customize + image: bitnami/mysql:8.0 env: - MYSQL_ALLOW_EMPTY_PASSWORD: true + ALLOW_EMPTY_PASSWORD: true MYSQL_DATABASE: testgitea ports: - "3306:3306" + options: >- + --mount type=tmpfs,destination=/bitnami/mysql/data elasticsearch: image: elasticsearch:7.5.0 env: @@ -188,7 +191,8 @@ jobs: - name: run migration tests run: make test-mysql-migration - name: run tests - run: make integration-test-coverage + # run: make integration-test-coverage (at the moment, no coverage is really handled) + run: make test-mysql env: TAGS: bindata RACE_ENABLED: true diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 7d5b3961bc..ef5684237d 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1912,7 +1912,7 @@ LEVEL = Info ;ENABLED = true ;; ;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. -;ALLOWED_TYPES = .csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip +;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip ;; ;; Max size of each file. Defaults to 2048MB ;MAX_SIZE = 2048 diff --git a/models/db/collation.go b/models/db/collation.go index c128cf5029..a7db9f5442 100644 --- a/models/db/collation.go +++ b/models/db/collation.go @@ -68,7 +68,8 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) { var candidateCollations []string if x.Dialect().URI().DBType == schemas.MYSQL { - if _, err = x.SQL("SELECT @@collation_database").Get(&res.DatabaseCollation); err != nil { + _, err = x.SQL("SELECT DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?", setting.Database.Name).Get(&res.DatabaseCollation) + if err != nil { return nil, err } res.IsCollationCaseSensitive = func(s string) bool { diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index b7970cb7c8..bbb028eb7b 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -26,7 +26,7 @@ fork_id: 0 is_template: false template_id: 0 - size: 8478 + size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 85cafc1ab9..ddf9a544da 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "os" - "path" "path/filepath" "runtime" "testing" @@ -16,7 +15,6 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/testlogger" @@ -35,27 +33,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu ourSkip := 2 ourSkip += skip deferFn := testlogger.PrintCurrentTest(t, ourSkip) - assert.NoError(t, os.RemoveAll(setting.RepoRootPath)) - assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) - ownerDirs, err := os.ReadDir(setting.RepoRootPath) - if err != nil { - assert.NoError(t, err, "unable to read the new repo root: %v\n", err) - } - for _, ownerDir := range ownerDirs { - if !ownerDir.Type().IsDir() { - continue - } - repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name())) - if err != nil { - assert.NoError(t, err, "unable to read the new repo root: %v\n", err) - } - for _, repoDir := range repoDirs { - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755) - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755) - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755) - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755) - } - } + assert.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) if err := deleteDB(); err != nil { t.Errorf("unable to reset database: %v", err) @@ -112,39 +90,36 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu } func MainTest(m *testing.M) { - log.RegisterEventWriter("test", testlogger.NewTestLoggerWriter) + testlogger.Init() giteaRoot := base.SetupGiteaRoot() if giteaRoot == "" { - fmt.Println("Environment variable $GITEA_ROOT not set") - os.Exit(1) + testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n") } giteaBinary := "gitea" if runtime.GOOS == "windows" { giteaBinary += ".exe" } - setting.AppPath = path.Join(giteaRoot, giteaBinary) + setting.AppPath = filepath.Join(giteaRoot, giteaBinary) if _, err := os.Stat(setting.AppPath); err != nil { - fmt.Printf("Could not find gitea binary at %s\n", setting.AppPath) - os.Exit(1) + testlogger.Fatalf("Could not find gitea binary at %s\n", setting.AppPath) } giteaConf := os.Getenv("GITEA_CONF") if giteaConf == "" { - giteaConf = path.Join(filepath.Dir(setting.AppPath), "tests/sqlite.ini") + giteaConf = filepath.Join(filepath.Dir(setting.AppPath), "tests/sqlite.ini") fmt.Printf("Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) } - if !path.IsAbs(giteaConf) { - setting.CustomConf = path.Join(giteaRoot, giteaConf) + if !filepath.IsAbs(giteaConf) { + setting.CustomConf = filepath.Join(giteaRoot, giteaConf) } else { setting.CustomConf = giteaConf } tmpDataPath, err := os.MkdirTemp("", "data") if err != nil { - fmt.Printf("Unable to create temporary data path %v\n", err) - os.Exit(1) + testlogger.Fatalf("Unable to create temporary data path %v\n", err) } setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") @@ -152,8 +127,7 @@ func MainTest(m *testing.M) { unittest.InitSettings() if err = git.InitFull(context.Background()); err != nil { - fmt.Printf("Unable to InitFull: %v\n", err) - os.Exit(1) + testlogger.Fatalf("Unable to InitFull: %v\n", err) } setting.LoadDBSetting() setting.InitLoggersForTest() diff --git a/models/repo/fork.go b/models/repo/fork.go index 07cd31c269..1c75e86458 100644 --- a/models/repo/fork.go +++ b/models/repo/fork.go @@ -54,21 +54,6 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error) return &forkedRepo, nil } -// GetForks returns all the forks of the repository -func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) { - sess := db.GetEngine(ctx) - - var forks []*Repository - if listOptions.Page == 0 { - forks = make([]*Repository, 0, repo.NumForks) - } else { - forks = make([]*Repository, 0, listOptions.PageSize) - sess = db.SetSessionPagination(sess, &listOptions) - } - - return forks, sess.Find(&forks, &Repository{ForkID: repo.ID}) -} - // IncrementRepoForkNum increment repository fork number func IncrementRepoForkNum(ctx context.Context, repoID int64) error { _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID) diff --git a/models/repo/pushmirror.go b/models/repo/pushmirror.go index bf134abfb1..55e8f3a068 100644 --- a/models/repo/pushmirror.go +++ b/models/repo/pushmirror.go @@ -9,15 +9,13 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) -// ErrPushMirrorNotExist mirror does not exist error -var ErrPushMirrorNotExist = util.NewNotExistErrorf("PushMirror does not exist") - // PushMirror represents mirror information of a repository. type PushMirror struct { ID int64 `xorm:"pk autoincr"` @@ -96,26 +94,46 @@ func DeletePushMirrors(ctx context.Context, opts PushMirrorOptions) error { return util.NewInvalidArgumentErrorf("repoID required and must be set") } +type findPushMirrorOptions struct { + db.ListOptions + RepoID int64 + SyncOnCommit optional.Option[bool] +} + +func (opts findPushMirrorOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.SyncOnCommit.Has() { + cond = cond.And(builder.Eq{"sync_on_commit": opts.SyncOnCommit.Value()}) + } + return cond +} + // GetPushMirrorsByRepoID returns push-mirror information of a repository. func GetPushMirrorsByRepoID(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*PushMirror, int64, error) { - sess := db.GetEngine(ctx).Where("repo_id = ?", repoID) - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - mirrors := make([]*PushMirror, 0, listOptions.PageSize) - count, err := sess.FindAndCount(&mirrors) - return mirrors, count, err + return db.FindAndCount[PushMirror](ctx, findPushMirrorOptions{ + ListOptions: listOptions, + RepoID: repoID, + }) +} + +func GetPushMirrorByIDAndRepoID(ctx context.Context, id, repoID int64) (*PushMirror, bool, error) { + var pushMirror PushMirror + has, err := db.GetEngine(ctx).Where("id = ?", id).And("repo_id = ?", repoID).Get(&pushMirror) + if !has || err != nil { + return nil, has, err } - mirrors := make([]*PushMirror, 0, 10) - count, err := sess.FindAndCount(&mirrors) - return mirrors, count, err + return &pushMirror, true, nil } // GetPushMirrorsSyncedOnCommit returns push-mirrors for this repo that should be updated by new commits func GetPushMirrorsSyncedOnCommit(ctx context.Context, repoID int64) ([]*PushMirror, error) { - mirrors := make([]*PushMirror, 0, 10) - return mirrors, db.GetEngine(ctx). - Where("repo_id = ? AND sync_on_commit = ?", repoID, true). - Find(&mirrors) + return db.Find[PushMirror](ctx, findPushMirrorOptions{ + RepoID: repoID, + SyncOnCommit: optional.Some(true), + }) } // PushMirrorsIterate iterates all push-mirror repositories. diff --git a/models/repo/repo.go b/models/repo/repo.go index 4776ff0b9c..7d78cee287 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "html/template" + "maps" "net" "net/url" "path/filepath" @@ -165,10 +166,10 @@ type Repository struct { Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` - RenderingMetas map[string]string `xorm:"-"` - DocumentRenderingMetas map[string]string `xorm:"-"` - Units []*RepoUnit `xorm:"-"` - PrimaryLanguage *LanguageStat `xorm:"-"` + commonRenderingMetas map[string]string `xorm:"-"` + + Units []*RepoUnit `xorm:"-"` + PrimaryLanguage *LanguageStat `xorm:"-"` IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"` ForkID int64 `xorm:"INDEX"` @@ -473,9 +474,8 @@ func (repo *Repository) MustOwner(ctx context.Context) *user_model.User { return repo.Owner } -// ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers. -func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { - if len(repo.RenderingMetas) == 0 { +func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]string { + if len(repo.commonRenderingMetas) == 0 { metas := map[string]string{ "user": repo.OwnerName, "repo": repo.Name, @@ -508,21 +508,34 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { metas["org"] = strings.ToLower(repo.OwnerName) } - repo.RenderingMetas = metas + repo.commonRenderingMetas = metas } - return repo.RenderingMetas + return repo.commonRenderingMetas } -// ComposeDocumentMetas composes a map of metas for properly rendering documents +// ComposeMetas composes a map of metas for properly rendering comments or comment-like contents (commit message) +func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { + metas := maps.Clone(repo.composeCommonMetas(ctx)) + metas["markdownLineBreakStyle"] = "comment" + metas["markupAllowShortIssuePattern"] = "true" + return metas +} + +// ComposeWikiMetas composes a map of metas for properly rendering wikis +func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string { + // does wiki need the "teams" and "org" from common metas? + metas := maps.Clone(repo.composeCommonMetas(ctx)) + metas["markdownLineBreakStyle"] = "document" + metas["markupAllowShortIssuePattern"] = "true" + return metas +} + +// ComposeDocumentMetas composes a map of metas for properly rendering documents (repo files) func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string { - if len(repo.DocumentRenderingMetas) == 0 { - metas := map[string]string{} - for k, v := range repo.ComposeMetas(ctx) { - metas[k] = v - } - repo.DocumentRenderingMetas = metas - } - return repo.DocumentRenderingMetas + // does document(file) need the "teams" and "org" from common metas? + metas := maps.Clone(repo.composeCommonMetas(ctx)) + metas["markdownLineBreakStyle"] = "document" + return metas } // GetBaseRepo populates repo.BaseRepo for a fork repository and diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 1bffadbf0a..9bed2e9197 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -98,8 +98,7 @@ func (repos RepositoryList) IDs() []int64 { return repoIDs } -// LoadAttributes loads the attributes for the given RepositoryList -func (repos RepositoryList) LoadAttributes(ctx context.Context) error { +func (repos RepositoryList) LoadOwners(ctx context.Context) error { if len(repos) == 0 { return nil } @@ -107,10 +106,6 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error { userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) { return repo.OwnerID, true }) - repoIDs := make([]int64, len(repos)) - for i := range repos { - repoIDs[i] = repos[i].ID - } // Load owners. users := make(map[int64]*user_model.User, len(userIDs)) @@ -123,12 +118,19 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error { for i := range repos { repos[i].Owner = users[repos[i].OwnerID] } + return nil +} + +func (repos RepositoryList) LoadLanguageStats(ctx context.Context) error { + if len(repos) == 0 { + return nil + } // Load primary language. stats := make(LanguageStatList, 0, len(repos)) if err := db.GetEngine(ctx). Where("`is_primary` = ? AND `language` != ?", true, "other"). - In("`repo_id`", repoIDs). + In("`repo_id`", repos.IDs()). Find(&stats); err != nil { return fmt.Errorf("find primary languages: %w", err) } @@ -141,10 +143,18 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error { } } } - return nil } +// LoadAttributes loads the attributes for the given RepositoryList +func (repos RepositoryList) LoadAttributes(ctx context.Context) error { + if err := repos.LoadOwners(ctx); err != nil { + return err + } + + return repos.LoadLanguageStats(ctx) +} + // SearchRepoOptions holds the search options type SearchRepoOptions struct { db.ListOptions diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index c13b698abf..6468e0f605 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -1,13 +1,12 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repo_test +package repo import ( "testing" "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -20,18 +19,18 @@ import ( ) var ( - countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10} - countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)} - countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)} + countRepospts = CountRepositoryOptions{OwnerID: 10} + countReposptsPublic = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)} + countReposptsPrivate = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)} ) func TestGetRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) ctx := db.DefaultContext - count, err1 := repo_model.CountRepositories(ctx, countRepospts) - privateCount, err2 := repo_model.CountRepositories(ctx, countReposptsPrivate) - publicCount, err3 := repo_model.CountRepositories(ctx, countReposptsPublic) + count, err1 := CountRepositories(ctx, countRepospts) + privateCount, err2 := CountRepositories(ctx, countReposptsPrivate) + publicCount, err3 := CountRepositories(ctx, countReposptsPublic) assert.NoError(t, err1) assert.NoError(t, err2) assert.NoError(t, err3) @@ -42,7 +41,7 @@ func TestGetRepositoryCount(t *testing.T) { func TestGetPublicRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPublic) + count, err := CountRepositories(db.DefaultContext, countReposptsPublic) assert.NoError(t, err) assert.Equal(t, int64(1), count) } @@ -50,14 +49,14 @@ func TestGetPublicRepositoryCount(t *testing.T) { func TestGetPrivateRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPrivate) + count, err := CountRepositories(db.DefaultContext, countReposptsPrivate) assert.NoError(t, err) assert.Equal(t, int64(2), count) } func TestRepoAPIURL(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 10}) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL()) } @@ -65,22 +64,22 @@ func TestRepoAPIURL(t *testing.T) { func TestWatchRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) + assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &Watch{RepoID: repo.ID, UserID: user.ID}) + unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID}) - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false)) - unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) + assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &Watch{RepoID: repo.ID, UserID: user.ID}) + unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID}) } func TestMetas(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := &repo_model.Repository{Name: "testRepo"} + repo := &Repository{Name: "testRepo"} repo.Owner = &user_model.User{Name: "testOwner"} repo.OwnerName = repo.Owner.Name @@ -90,16 +89,16 @@ func TestMetas(t *testing.T) { assert.Equal(t, "testRepo", metas["repo"]) assert.Equal(t, "testOwner", metas["user"]) - externalTracker := repo_model.RepoUnit{ + externalTracker := RepoUnit{ Type: unit.TypeExternalTracker, - Config: &repo_model.ExternalTrackerConfig{ + Config: &ExternalTrackerConfig{ ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}", }, } testSuccess := func(expectedStyle string) { - repo.Units = []*repo_model.RepoUnit{&externalTracker} - repo.RenderingMetas = nil + repo.Units = []*RepoUnit{&externalTracker} + repo.commonRenderingMetas = nil metas := repo.ComposeMetas(db.DefaultContext) assert.Equal(t, expectedStyle, metas["style"]) assert.Equal(t, "testRepo", metas["repo"]) @@ -118,7 +117,7 @@ func TestMetas(t *testing.T) { externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp testSuccess(markup.IssueNameStyleRegexp) - repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 3) + repo, err := GetRepositoryByID(db.DefaultContext, 3) assert.NoError(t, err) metas = repo.ComposeMetas(db.DefaultContext) @@ -132,7 +131,7 @@ func TestGetRepositoryByURL(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) t.Run("InvalidPath", func(t *testing.T) { - repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, "something") + repo, err := GetRepositoryByURL(db.DefaultContext, "something") assert.Nil(t, repo) assert.Error(t, err) @@ -140,7 +139,7 @@ func TestGetRepositoryByURL(t *testing.T) { t.Run("ValidHttpURL", func(t *testing.T) { test := func(t *testing.T, url string) { - repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url) + repo, err := GetRepositoryByURL(db.DefaultContext, url) assert.NotNil(t, repo) assert.NoError(t, err) @@ -155,7 +154,7 @@ func TestGetRepositoryByURL(t *testing.T) { t.Run("ValidGitSshURL", func(t *testing.T) { test := func(t *testing.T, url string) { - repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url) + repo, err := GetRepositoryByURL(db.DefaultContext, url) assert.NotNil(t, repo) assert.NoError(t, err) @@ -173,7 +172,7 @@ func TestGetRepositoryByURL(t *testing.T) { t.Run("ValidImplicitSshURL", func(t *testing.T) { test := func(t *testing.T, url string) { - repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url) + repo, err := GetRepositoryByURL(db.DefaultContext, url) assert.NotNil(t, repo) assert.NoError(t, err) @@ -200,21 +199,21 @@ func TestComposeSSHCloneURL(t *testing.T) { setting.SSH.Domain = "domain" setting.SSH.Port = 22 setting.Repository.UseCompatSSHURI = false - assert.Equal(t, "git@domain:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL("user", "repo")) setting.Repository.UseCompatSSHURI = true - assert.Equal(t, "ssh://git@domain/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL("user", "repo")) // test SSH_DOMAIN while use non-standard SSH port setting.SSH.Port = 123 setting.Repository.UseCompatSSHURI = false - assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) setting.Repository.UseCompatSSHURI = true - assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) // test IPv6 SSH_DOMAIN setting.Repository.UseCompatSSHURI = false setting.SSH.Domain = "::1" setting.SSH.Port = 22 - assert.Equal(t, "git@[::1]:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL("user", "repo")) setting.SSH.Port = 123 - assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) } diff --git a/models/repo/search.go b/models/repo/search.go index a73d9fc215..ffb8e26745 100644 --- a/models/repo/search.go +++ b/models/repo/search.go @@ -36,6 +36,7 @@ var OrderByMap = map[string]map[string]db.SearchOrderBy{ var OrderByFlatMap = map[string]db.SearchOrderBy{ "newest": OrderByMap["desc"]["created"], "oldest": OrderByMap["asc"]["created"], + "recentupdate": OrderByMap["desc"]["updated"], "leastupdate": OrderByMap["asc"]["updated"], "reversealphabetically": OrderByMap["desc"]["alpha"], "alphabetically": OrderByMap["asc"]["alpha"], diff --git a/models/unittest/fscopy.go b/models/unittest/fscopy.go index 74b12d5057..4d7ee2151d 100644 --- a/models/unittest/fscopy.go +++ b/models/unittest/fscopy.go @@ -4,10 +4,8 @@ package unittest import ( - "errors" - "io" "os" - "path" + "path/filepath" "strings" "code.gitea.io/gitea/modules/util" @@ -32,67 +30,73 @@ func Copy(src, dest string) error { return os.Symlink(target, dest) } - sr, err := os.Open(src) - if err != nil { - return err - } - defer sr.Close() - - dw, err := os.Create(dest) - if err != nil { - return err - } - defer dw.Close() - - if _, err = io.Copy(dw, sr); err != nil { - return err - } - - // Set back file information. - if err = os.Chtimes(dest, si.ModTime(), si.ModTime()); err != nil { - return err - } - return os.Chmod(dest, si.Mode()) + return util.CopyFile(src, dest) } -// CopyDir copy files recursively from source to target directory. -// -// The filter accepts a function that process the path info. -// and should return true for need to filter. -// -// It returns error when error occurs in underlying functions. -func CopyDir(srcPath, destPath string, filters ...func(filePath string) bool) error { - // Check if target directory exists. - if _, err := os.Stat(destPath); !errors.Is(err, os.ErrNotExist) { - return util.NewAlreadyExistErrorf("file or directory already exists: %s", destPath) +// Sync synchronizes the two files. This is skipped if both files +// exist and the size, modtime, and mode match. +func Sync(srcPath, destPath string) error { + dest, err := os.Stat(destPath) + if err != nil { + if os.IsNotExist(err) { + return Copy(srcPath, destPath) + } + return err } + src, err := os.Stat(srcPath) + if err != nil { + return err + } + + if src.Size() == dest.Size() && + src.ModTime() == dest.ModTime() && + src.Mode() == dest.Mode() { + return nil + } + + return Copy(srcPath, destPath) +} + +// SyncDirs synchronizes files recursively from source to target directory. +// It returns error when error occurs in underlying functions. +func SyncDirs(srcPath, destPath string) error { err := os.MkdirAll(destPath, os.ModePerm) if err != nil { return err } - // Gather directory info. - infos, err := util.StatDir(srcPath, true) + // find and delete all untracked files + destFiles, err := util.StatDir(destPath, true) if err != nil { return err } - - var filter func(filePath string) bool - if len(filters) > 0 { - filter = filters[0] + for _, destFile := range destFiles { + destFilePath := filepath.Join(destPath, destFile) + if _, err = os.Stat(filepath.Join(srcPath, destFile)); err != nil { + if os.IsNotExist(err) { + // if src file does not exist, remove dest file + if err = os.RemoveAll(destFilePath); err != nil { + return err + } + } else { + return err + } + } } - for _, info := range infos { - if filter != nil && filter(info) { - continue - } - - curPath := path.Join(destPath, info) - if strings.HasSuffix(info, "/") { - err = os.MkdirAll(curPath, os.ModePerm) + // sync src files to dest + srcFiles, err := util.StatDir(srcPath, true) + if err != nil { + return err + } + for _, srcFile := range srcFiles { + destFilePath := filepath.Join(destPath, srcFile) + // util.StatDir appends a slash to the directory name + if strings.HasSuffix(srcFile, "/") { + err = os.MkdirAll(destFilePath, os.ModePerm) } else { - err = Copy(path.Join(srcPath, info), curPath) + err = Sync(filepath.Join(srcPath, srcFile), destFilePath) } if err != nil { return err diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 53c9dbdd77..5a1c27dbea 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -164,35 +164,13 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) { if err = storage.Init(); err != nil { fatalTestError("storage.Init: %v\n", err) } - if err = util.RemoveAll(repoRootPath); err != nil { - fatalTestError("util.RemoveAll: %v\n", err) - } - if err = CopyDir(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil { - fatalTestError("util.CopyDir: %v\n", err) + if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil { + fatalTestError("util.SyncDirs: %v\n", err) } if err = git.InitFull(context.Background()); err != nil { fatalTestError("git.Init: %v\n", err) } - ownerDirs, err := os.ReadDir(setting.RepoRootPath) - if err != nil { - fatalTestError("unable to read the new repo root: %v\n", err) - } - for _, ownerDir := range ownerDirs { - if !ownerDir.Type().IsDir() { - continue - } - repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name())) - if err != nil { - fatalTestError("unable to read the new repo root: %v\n", err) - } - for _, repoDir := range repoDirs { - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755) - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755) - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755) - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755) - } - } if len(testOpts) > 0 && testOpts[0].SetUp != nil { if err := testOpts[0].SetUp(); err != nil { @@ -255,24 +233,7 @@ func PrepareTestDatabase() error { // by tests that use the above MainTest(..) function. func PrepareTestEnv(t testing.TB) { assert.NoError(t, PrepareTestDatabase()) - assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta") - assert.NoError(t, CopyDir(metaPath, setting.RepoRootPath)) - ownerDirs, err := os.ReadDir(setting.RepoRootPath) - assert.NoError(t, err) - for _, ownerDir := range ownerDirs { - if !ownerDir.Type().IsDir() { - continue - } - repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name())) - assert.NoError(t, err) - for _, repoDir := range repoDirs { - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755) - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755) - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755) - _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755) - } - } - + assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath)) base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set } diff --git a/modules/git/tests/repos/language_stats_repo/description b/modules/git/tests/repos/language_stats_repo/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/modules/git/tests/repos/language_stats_repo/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/language_stats_repo/info/exclude b/modules/git/tests/repos/language_stats_repo/info/exclude deleted file mode 100644 index a5196d1be8..0000000000 --- a/modules/git/tests/repos/language_stats_repo/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/modules/git/tests/repos/repo1_bare/description b/modules/git/tests/repos/repo1_bare/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/modules/git/tests/repos/repo1_bare/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/repo1_bare/info/exclude b/modules/git/tests/repos/repo1_bare/info/exclude deleted file mode 100644 index a5196d1be8..0000000000 --- a/modules/git/tests/repos/repo1_bare/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/modules/git/tests/repos/repo1_bare_sha256/description b/modules/git/tests/repos/repo1_bare_sha256/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/modules/git/tests/repos/repo1_bare_sha256/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/repo1_bare_sha256/info/exclude b/modules/git/tests/repos/repo1_bare_sha256/info/exclude deleted file mode 100644 index a5196d1be8..0000000000 --- a/modules/git/tests/repos/repo1_bare_sha256/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/modules/git/tests/repos/repo2_empty/description b/modules/git/tests/repos/repo2_empty/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/modules/git/tests/repos/repo2_empty/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/repo2_empty/info/exclude b/modules/git/tests/repos/repo2_empty/info/exclude deleted file mode 100644 index a5196d1be8..0000000000 --- a/modules/git/tests/repos/repo2_empty/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/modules/git/tests/repos/repo3_notes/description b/modules/git/tests/repos/repo3_notes/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/modules/git/tests/repos/repo3_notes/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/repo5_pulls/description b/modules/git/tests/repos/repo5_pulls/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/modules/git/tests/repos/repo5_pulls/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/repo5_pulls/info/exclude b/modules/git/tests/repos/repo5_pulls/info/exclude deleted file mode 100644 index a5196d1be8..0000000000 --- a/modules/git/tests/repos/repo5_pulls/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/modules/git/tests/repos/repo5_pulls_sha256/description b/modules/git/tests/repos/repo5_pulls_sha256/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/modules/git/tests/repos/repo5_pulls_sha256/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/repo6_blame_sha256/description b/modules/git/tests/repos/repo6_blame_sha256/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/modules/git/tests/repos/repo6_blame_sha256/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/repo6_blame_sha256/info/exclude b/modules/git/tests/repos/repo6_blame_sha256/info/exclude deleted file mode 100644 index a5196d1be8..0000000000 --- a/modules/git/tests/repos/repo6_blame_sha256/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/modules/git/tests/repos/repo6_merge_sha256/description b/modules/git/tests/repos/repo6_merge_sha256/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/modules/git/tests/repos/repo6_merge_sha256/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/git/tests/repos/repo6_merge_sha256/info/exclude b/modules/git/tests/repos/repo6_merge_sha256/info/exclude deleted file mode 100644 index a5196d1be8..0000000000 --- a/modules/git/tests/repos/repo6_merge_sha256/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/modules/html/html.go b/modules/html/html.go deleted file mode 100644 index b1ebd584c6..0000000000 --- a/modules/html/html.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package html - -// ParseSizeAndClass get size and class from string with default values -// If present, "others" expects the new size first and then the classes to use -func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int, string) { - size := defaultSize - if len(others) >= 1 { - if v, ok := others[0].(int); ok && v != 0 { - size = v - } - } - class := defaultClass - if len(others) >= 2 { - if v, ok := others[1].(string); ok && v != "" { - if class != "" { - class += " " - } - class += v - } - } - return size, class -} diff --git a/modules/htmlutil/html.go b/modules/htmlutil/html.go new file mode 100644 index 0000000000..9b5f5a92d8 --- /dev/null +++ b/modules/htmlutil/html.go @@ -0,0 +1,48 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package htmlutil + +import ( + "fmt" + "html/template" + "slices" +) + +// ParseSizeAndClass get size and class from string with default values +// If present, "others" expects the new size first and then the classes to use +func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int, string) { + size := defaultSize + if len(others) >= 1 { + if v, ok := others[0].(int); ok && v != 0 { + size = v + } + } + class := defaultClass + if len(others) >= 2 { + if v, ok := others[1].(string); ok && v != "" { + if class != "" { + class += " " + } + class += v + } + } + return size, class +} + +func HTMLFormat(s string, rawArgs ...any) template.HTML { + args := slices.Clone(rawArgs) + for i, v := range args { + switch v := v.(type) { + case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML: + // for most basic types (including template.HTML which is safe), just do nothing and use it + case string: + args[i] = template.HTMLEscapeString(v) + case fmt.Stringer: + args[i] = template.HTMLEscapeString(v.String()) + default: + args[i] = template.HTMLEscapeString(fmt.Sprint(v)) + } + } + return template.HTML(fmt.Sprintf(s, args...)) +} diff --git a/modules/htmlutil/html_test.go b/modules/htmlutil/html_test.go new file mode 100644 index 0000000000..5ff05d75b3 --- /dev/null +++ b/modules/htmlutil/html_test.go @@ -0,0 +1,15 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package htmlutil + +import ( + "html/template" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHTMLFormat(t *testing.T) { + assert.Equal(t, template.HTML("< < 1"), HTMLFormat("%s %s %d", "<", template.HTML("<"), 1)) +} diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index 2e3e6a7c42..8fb667876e 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -46,7 +46,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { w.Header().Add(gzhttp.HeaderNoCompression, "1") } - contentType := typesniffer.ApplicationOctetStream + contentType := typesniffer.MimeTypeApplicationOctetStream if opts.ContentType != "" { if opts.ContentTypeCharset != "" { contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset) @@ -107,7 +107,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath stri } else if isPlain { opts.ContentType = "text/plain" } else { - opts.ContentType = typesniffer.ApplicationOctetStream + opts.ContentType = typesniffer.MimeTypeApplicationOctetStream } } diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index 020ccc72f8..78fbe7f792 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -21,6 +21,7 @@ import ( _ "code.gitea.io/gitea/models/activities" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" _ "github.com/mattn/go-sqlite3" ) @@ -284,15 +285,11 @@ func TestBleveIndexAndSearch(t *testing.T) { dir := t.TempDir() idx := bleve.NewIndexer(dir) - _, err := idx.Init(context.Background()) - if err != nil { - if idx != nil { - idx.Close() - } - assert.FailNow(t, "Unable to create bleve indexer Error: %v", err) - } defer idx.Close() + _, err := idx.Init(context.Background()) + require.NoError(t, err) + testIndexer("beleve", t, idx) } diff --git a/modules/log/color.go b/modules/log/color.go index dcbba5f6d6..2a37fd0ea9 100644 --- a/modules/log/color.go +++ b/modules/log/color.go @@ -86,6 +86,8 @@ type ColoredValue struct { colors []ColorAttribute } +var _ fmt.Formatter = (*ColoredValue)(nil) + func (c *ColoredValue) Format(f fmt.State, verb rune) { _, _ = f.Write(ColorBytes(c.colors...)) s := fmt.Sprintf(fmt.FormatString(f, verb), c.v) @@ -93,6 +95,10 @@ func (c *ColoredValue) Format(f fmt.State, verb rune) { _, _ = f.Write(resetBytes) } +func (c *ColoredValue) Value() any { + return c.v +} + func NewColoredValue(v any, color ...ColorAttribute) *ColoredValue { return &ColoredValue{v: v, colors: color} } diff --git a/modules/markup/asciicast/asciicast.go b/modules/markup/asciicast/asciicast.go index 0678062340..e92b78a4bc 100644 --- a/modules/markup/asciicast/asciicast.go +++ b/modules/markup/asciicast/asciicast.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "net/url" - "regexp" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" @@ -38,10 +37,7 @@ const ( // SanitizerRules implements markup.Renderer func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { - return []setting.MarkupSanitizerRule{ - {Element: "div", AllowAttr: "class", Regexp: regexp.MustCompile(playerClassName)}, - {Element: "div", AllowAttr: playerSrcAttr}, - } + return []setting.MarkupSanitizerRule{{Element: "div", AllowAttr: playerSrcAttr}} } // Render implements markup.Renderer @@ -53,12 +49,5 @@ func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) ctx.Metas["BranchNameSubURL"], url.PathEscape(ctx.RelativePath), ) - - _, err := io.WriteString(output, fmt.Sprintf( - `
`, - playerClassName, - playerSrcAttr, - rawURL, - )) - return err + return ctx.RenderInternal.FormatWithSafeAttrs(output, ``, playerClassName, playerSrcAttr, rawURL) } diff --git a/modules/markup/common/html.go b/modules/markup/common/html.go deleted file mode 100644 index 5658839c6f..0000000000 --- a/modules/markup/common/html.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package common - -import ( - "mvdan.cc/xurls/v2" -) - -// NOTE: All below regex matching do not perform any extra validation. -// Thus a link is produced even if the linked entity does not exist. -// While fast, this is also incorrect and lead to false positives. -// TODO: fix invalid linking issue - -// LinkRegex is a regexp matching a valid link -var LinkRegex, _ = xurls.StrictMatchingScheme("https?://") diff --git a/modules/markup/common/linkify.go b/modules/markup/common/linkify.go index f84680205e..be6ab22b55 100644 --- a/modules/markup/common/linkify.go +++ b/modules/markup/common/linkify.go @@ -9,15 +9,27 @@ package common import ( "bytes" "regexp" + "sync" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" + "mvdan.cc/xurls/v2" ) -var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`) +type GlobalVarsType struct { + wwwURLRegxp *regexp.Regexp + LinkRegex *regexp.Regexp // fast matching a URL link, no any extra validation. +} + +var GlobalVars = sync.OnceValue[*GlobalVarsType](func() *GlobalVarsType { + v := &GlobalVarsType{} + v.wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`) + v.LinkRegex, _ = xurls.StrictMatchingScheme("https?://") + return v +}) type linkifyParser struct{} @@ -60,10 +72,10 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont var protocol []byte typ := ast.AutoLinkURL if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) { - m = LinkRegex.FindSubmatchIndex(line) + m = GlobalVars().LinkRegex.FindSubmatchIndex(line) } if m == nil && bytes.HasPrefix(line, domainWWW) { - m = wwwURLRegxp.FindSubmatchIndex(line) + m = GlobalVars().wwwURLRegxp.FindSubmatchIndex(line) protocol = []byte("http") } if m != nil { diff --git a/modules/markup/console/console.go b/modules/markup/console/console.go index d991527b80..06f3acfa68 100644 --- a/modules/markup/console/console.go +++ b/modules/markup/console/console.go @@ -6,8 +6,7 @@ package console import ( "bytes" "io" - "path/filepath" - "regexp" + "path" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" @@ -36,7 +35,7 @@ func (Renderer) Extensions() []string { // SanitizerRules implements markup.Renderer func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{ - {Element: "span", AllowAttr: "class", Regexp: regexp.MustCompile(`^term-((fg[ix]?|bg)\d+|container)$`)}, + {Element: "span", AllowAttr: "class", Regexp: `^term-((fg[ix]?|bg)\d+|container)$`}, } } @@ -46,7 +45,7 @@ func (Renderer) CanRender(filename string, input io.Reader) bool { if err != nil { return false } - if enry.GetLanguage(filepath.Base(filename), buf) != enry.OtherLanguage { + if enry.GetLanguage(path.Base(filename), buf) != enry.OtherLanguage { return false } return bytes.ContainsRune(buf, '\x1b') diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go index 3d952b0de4..a3e6bbaac6 100644 --- a/modules/markup/csv/csv.go +++ b/modules/markup/csv/csv.go @@ -7,7 +7,6 @@ import ( "bufio" "html" "io" - "regexp" "strconv" "code.gitea.io/gitea/modules/csv" @@ -37,9 +36,9 @@ func (Renderer) Extensions() []string { // SanitizerRules implements markup.Renderer func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{ - {Element: "table", AllowAttr: "class", Regexp: regexp.MustCompile(`data-table`)}, - {Element: "th", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)}, - {Element: "td", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)}, + {Element: "table", AllowAttr: "class", Regexp: `^data-table$`}, + {Element: "th", AllowAttr: "class", Regexp: `^line-num$`}, + {Element: "td", AllowAttr: "class", Regexp: `^line-num$`}, } } @@ -51,13 +50,13 @@ func writeField(w io.Writer, element, class, field string) error { return err } if len(class) > 0 { - if _, err := io.WriteString(w, " class=\""); err != nil { + if _, err := io.WriteString(w, ` class="`); err != nil { return err } if _, err := io.WriteString(w, class); err != nil { return err } - if _, err := io.WriteString(w, "\""); err != nil { + if _, err := io.WriteString(w, `"`); err != nil { return err } } diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go index 122517ed11..d28dc9fa5d 100644 --- a/modules/markup/external/external.go +++ b/modules/markup/external/external.go @@ -102,7 +102,7 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io. _, err = io.Copy(f, input) if err != nil { - f.Close() + _ = f.Close() return fmt.Errorf("%s write data to temp file when rendering %s failed: %w", p.Name(), p.Command, err) } @@ -113,10 +113,9 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io. args = append(args, f.Name()) } - if ctx == nil || ctx.Ctx == nil { - if ctx == nil { - log.Warn("RenderContext not provided defaulting to empty ctx") - ctx = &markup.RenderContext{} + if ctx.Ctx == nil { + if !setting.IsProd || setting.IsInTesting { + panic("RenderContext did not provide context") } log.Warn("RenderContext did not provide context, defaulting to Shutdown context") ctx.Ctx = graceful.GetManager().ShutdownContext() diff --git a/modules/markup/html.go b/modules/markup/html.go index 54c65c95d2..e8799c401c 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -7,11 +7,11 @@ import ( "bytes" "io" "regexp" + "slices" "strings" "sync" "code.gitea.io/gitea/modules/markup/common" - "code.gitea.io/gitea/modules/setting" "golang.org/x/net/html" "golang.org/x/net/html/atom" @@ -25,7 +25,25 @@ const ( IssueNameStyleRegexp = "regexp" ) -var ( +type globalVarsType struct { + hashCurrentPattern *regexp.Regexp + shortLinkPattern *regexp.Regexp + anyHashPattern *regexp.Regexp + comparePattern *regexp.Regexp + fullURLPattern *regexp.Regexp + emailRegex *regexp.Regexp + blackfridayExtRegex *regexp.Regexp + emojiShortCodeRegex *regexp.Regexp + issueFullPattern *regexp.Regexp + filesChangedFullPattern *regexp.Regexp + codePreviewPattern *regexp.Regexp + + tagCleaner *regexp.Regexp + nulCleaner *strings.Replacer +} + +var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType { + v := &globalVarsType{} // NOTE: All below regex matching do not perform any extra validation. // Thus a link is produced even if the linked entity does not exist. // While fast, this is also incorrect and lead to false positives. @@ -36,79 +54,59 @@ var ( // hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae // Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length // so that abbreviated hash links can be used as well. This matches git and GitHub usability. - hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`) + v.hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`) // shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax - shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) + v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) // anyHashPattern splits url containing SHA into parts - anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) + v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) // comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash" - comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) + v.comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) // fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..." - fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`) + v.fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`) // emailRegex is definitely not perfect with edge cases, // it is still accepted by the CommonMark specification, as well as the HTML5 spec: // http://spec.commonmark.org/0.28/#email-address // https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail) - emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))") + v.emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))") // blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote - blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) + v.blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) // emojiShortCodeRegex find emoji by alias like :smile: - emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) -) + v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) -// CSS class for action keywords (e.g. "closes: #1") -const keywordClass = "issue-keyword" + // example: https://domain/org/repo/pulls/27#hash + v.issueFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`) + + // example: https://domain/org/repo/pulls/27/files#hash + v.filesChangedFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`) + + // codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20" + v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`) + + v.tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`) + v.nulCleaner = strings.NewReplacer("\000", "") + return v +}) // IsFullURLBytes reports whether link fits valid format. func IsFullURLBytes(link []byte) bool { - return fullURLPattern.Match(link) + return globalVars().fullURLPattern.Match(link) } func IsFullURLString(link string) bool { - return fullURLPattern.MatchString(link) + return globalVars().fullURLPattern.MatchString(link) } func IsNonEmptyRelativePath(link string) bool { return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#' } -// regexp for full links to issues/pulls -var issueFullPattern *regexp.Regexp - -// Once for to prevent races -var issueFullPatternOnce sync.Once - -// regexp for full links to hash comment in pull request files changed tab -var filesChangedFullPattern *regexp.Regexp - -// Once for to prevent races -var filesChangedFullPatternOnce sync.Once - -func getIssueFullPattern() *regexp.Regexp { - issueFullPatternOnce.Do(func() { - // example: https://domain/org/repo/pulls/27#hash - issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) + - `[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`) - }) - return issueFullPattern -} - -func getFilesChangedFullPattern() *regexp.Regexp { - filesChangedFullPatternOnce.Do(func() { - // example: https://domain/org/repo/pulls/27/files#hash - filesChangedFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) + - `[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`) - }) - return filesChangedFullPattern -} - // CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text func CustomLinkURLSchemes(schemes []string) { schemes = append(schemes, "http", "https") @@ -132,7 +130,7 @@ func CustomLinkURLSchemes(schemes []string) { } withAuth = append(withAuth, s) } - common.LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|")) + common.GlobalVars().LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|")) } type postProcessError struct { @@ -167,11 +165,7 @@ var defaultProcessors = []processor{ // emails with HTML links, parsing shortlinks in the format of [[Link]], like // MediaWiki, linking issues in the format #ID, and mentions in the format // @user, and others. -func PostProcess( - ctx *RenderContext, - input io.Reader, - output io.Writer, -) error { +func PostProcess(ctx *RenderContext, input io.Reader, output io.Writer) error { return postProcess(ctx, defaultProcessors, input, output) } @@ -192,18 +186,8 @@ var commitMessageProcessors = []processor{ // RenderCommitMessage will use the same logic as PostProcess, but will disable // the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is // set, which changes every text node into a link to the passed default link. -func RenderCommitMessage( - ctx *RenderContext, - content string, -) (string, error) { +func RenderCommitMessage(ctx *RenderContext, content string) (string, error) { procs := commitMessageProcessors - if ctx.DefaultLink != "" { - // we don't have to fear data races, because being - // commitMessageProcessors of fixed len and cap, every time we append - // something to it the slice is realloc+copied, so append always - // generates the slice ex-novo. - procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink)) - } return renderProcessString(ctx, procs, content) } @@ -229,30 +213,23 @@ var emojiProcessors = []processor{ // RenderCommitMessage, but will disable the shortLinkProcessor and // emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set, // which changes every text node into a link to the passed default link. -func RenderCommitMessageSubject( - ctx *RenderContext, - content string, -) (string, error) { - procs := commitMessageSubjectProcessors - if ctx.DefaultLink != "" { - // we don't have to fear data races, because being - // commitMessageSubjectProcessors of fixed len and cap, every time we - // append something to it the slice is realloc+copied, so append always - // generates the slice ex-novo. - procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink)) - } +func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) { + procs := slices.Clone(commitMessageSubjectProcessors) + procs = append(procs, func(ctx *RenderContext, node *html.Node) { + ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data} + node.Type = html.ElementNode + node.Data = "a" + node.DataAtom = atom.A + node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}} + node.FirstChild, node.LastChild = ch, ch + }) return renderProcessString(ctx, procs, content) } // RenderIssueTitle to process title on individual issue/pull page -func RenderIssueTitle( - ctx *RenderContext, - title string, -) (string, error) { +func RenderIssueTitle(ctx *RenderContext, title string) (string, error) { + // do not render other issue/commit links in an issue's title - which in most cases is already a link. return renderProcessString(ctx, []processor{ - issueIndexPatternProcessor, - commitCrossReferencePatternProcessor, - hashCurrentPatternProcessor, emojiShortCodeProcessor, emojiProcessor, }, title) @@ -268,10 +245,7 @@ func renderProcessString(ctx *RenderContext, procs []processor, content string) // RenderDescriptionHTML will use similar logic as PostProcess, but will // use a single special linkProcessor. -func RenderDescriptionHTML( - ctx *RenderContext, - content string, -) (string, error) { +func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) { return renderProcessString(ctx, []processor{ descriptionLinkProcessor, emojiShortCodeProcessor, @@ -281,18 +255,10 @@ func RenderDescriptionHTML( // RenderEmoji for when we want to just process emoji and shortcodes // in various places it isn't already run through the normal markdown processor -func RenderEmoji( - ctx *RenderContext, - content string, -) (string, error) { +func RenderEmoji(ctx *RenderContext, content string) (string, error) { return renderProcessString(ctx, emojiProcessors, content) } -var ( - tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`) - nulCleaner = strings.NewReplacer("\000", "") -) - func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error { defer ctx.Cancel() // FIXME: don't read all content to memory @@ -306,7 +272,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output // prepend "" strings.NewReader(""), // Strip out nuls - they're always invalid - bytes.NewReader(tagCleaner.ReplaceAll([]byte(nulCleaner.Replace(string(rawHTML))), []byte("<$1"))), + bytes.NewReader(globalVars().tagCleaner.ReplaceAll([]byte(globalVars().nulCleaner.Replace(string(rawHTML))), []byte("<$1"))), // close the tags strings.NewReader(""), )) @@ -349,11 +315,22 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output return nil } +func isEmojiNode(node *html.Node) bool { + if node.Type == html.ElementNode && node.Data == atom.Span.String() { + for _, attr := range node.Attr { + if (attr.Key == "class" || attr.Key == "data-attr-class") && strings.Contains(attr.Val, "emoji") { + return true + } + } + } + return false +} + func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node { // Add user-content- to IDs and "#" links if they don't already have them for idx, attr := range node.Attr { val := strings.TrimPrefix(attr.Val, "#") - notHasPrefix := !(strings.HasPrefix(val, "user-content-") || blackfridayExtRegex.MatchString(val)) + notHasPrefix := !(strings.HasPrefix(val, "user-content-") || globalVars().blackfridayExtRegex.MatchString(val)) if attr.Key == "id" && notHasPrefix { node.Attr[idx].Val = "user-content-" + attr.Val @@ -362,47 +339,27 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix { node.Attr[idx].Val = "#user-content-" + val } - - if attr.Key == "class" && attr.Val == "emoji" { - procs = nil - } } switch node.Type { case html.TextNode: - processTextNodes(ctx, procs, node) + for _, proc := range procs { + proc(ctx, node) // it might add siblings + } + case html.ElementNode: - if node.Data == "code" || node.Data == "pre" { - // ignore code and pre nodes + if isEmojiNode(node) { + // TextNode emoji will be converted to ``, then the next iteration will visit the "span" + // if we don't stop it, it will go into the TextNode again and create an infinite recursion return node.NextSibling + } else if node.Data == "code" || node.Data == "pre" { + return node.NextSibling // ignore code and pre nodes } else if node.Data == "img" { return visitNodeImg(ctx, node) } else if node.Data == "video" { return visitNodeVideo(ctx, node) } else if node.Data == "a" { - // Restrict text in links to emojis - procs = emojiProcessors - } else if node.Data == "i" { - for _, attr := range node.Attr { - if attr.Key != "class" { - continue - } - classes := strings.Split(attr.Val, " ") - for i, class := range classes { - if class == "icon" { - classes[0], classes[i] = classes[i], classes[0] - attr.Val = strings.Join(classes, " ") - - // Remove all children of icons - child := node.FirstChild - for child != nil { - node.RemoveChild(child) - child = node.FirstChild - } - break - } - } - } + procs = emojiProcessors // Restrict text in links to emojis } for n := node.FirstChild; n != nil; { n = visitNode(ctx, procs, n) @@ -412,22 +369,17 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod return node.NextSibling } -// processTextNodes runs the passed node through various processors, in order to handle -// all kinds of special links handled by the post-processing. -func processTextNodes(ctx *RenderContext, procs []processor, node *html.Node) { - for _, p := range procs { - p(ctx, node) - } -} - // createKeyword() renders a highlighted version of an action keyword -func createKeyword(content string) *html.Node { +func createKeyword(ctx *RenderContext, content string) *html.Node { + // CSS class for action keywords (e.g. "closes: #1") + const keywordClass = "issue-keyword" + span := &html.Node{ Type: html.ElementNode, Data: atom.Span.String(), Attr: []html.Attribute{}, } - span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: keywordClass}) + span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", keywordClass)) text := &html.Node{ Type: html.TextNode, @@ -438,7 +390,7 @@ func createKeyword(content string) *html.Node { return span } -func createLink(href, content, class string) *html.Node { +func createLink(ctx *RenderContext, href, content, class string) *html.Node { a := &html.Node{ Type: html.ElementNode, Data: atom.A.String(), @@ -448,7 +400,7 @@ func createLink(href, content, class string) *html.Node { a.Attr = append(a.Attr, html.Attribute{Key: "data-markdown-generated-content"}) } if class != "" { - a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class}) + a.Attr = append(a.Attr, ctx.RenderInternal.NodeSafeAttr("class", class)) } text := &html.Node{ diff --git a/modules/markup/html_codepreview.go b/modules/markup/html_codepreview.go index 5ab9290b3e..5c88481d76 100644 --- a/modules/markup/html_codepreview.go +++ b/modules/markup/html_codepreview.go @@ -6,7 +6,6 @@ package markup import ( "html/template" "net/url" - "regexp" "strconv" "strings" @@ -16,9 +15,6 @@ import ( "golang.org/x/net/html" ) -// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20" -var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`) - type RenderCodePreviewOptions struct { FullURL string OwnerName string @@ -30,7 +26,7 @@ type RenderCodePreviewOptions struct { } func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) { - m := codePreviewPattern.FindStringSubmatchIndex(node.Data) + m := globalVars().codePreviewPattern.FindStringSubmatchIndex(node.Data) if m == nil { return 0, 0, "", nil } @@ -66,8 +62,8 @@ func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling continue } - urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node) - if err != nil || h == "" { + urlPosStart, urlPosEnd, renderedCodeBlock, err := renderCodeBlock(ctx, node) + if err != nil || renderedCodeBlock == "" { if err != nil { log.Error("Unable to render code preview: %v", err) } @@ -84,7 +80,8 @@ func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { // then it is resolved as: "{TextBefore}
{TextAfter}
", // so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node. node.Data = textBefore - node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next) + renderedCodeNode := &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(renderedCodeBlock))} + node.Parent.InsertBefore(renderedCodeNode, next) if textAfter != "" { node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next) } diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go index 86d70746d4..0e674c83e1 100644 --- a/modules/markup/html_commit.go +++ b/modules/markup/html_commit.go @@ -54,7 +54,7 @@ func createCodeLink(href, content, class string) *html.Node { } func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) { - m := anyHashPattern.FindStringSubmatchIndex(s) + m := globalVars().anyHashPattern.FindStringSubmatchIndex(s) if m == nil { return ret, false } @@ -120,7 +120,7 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling continue } - m := comparePattern.FindStringSubmatchIndex(node.Data) + m := globalVars().comparePattern.FindStringSubmatchIndex(node.Data) if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match node = node.NextSibling continue @@ -173,7 +173,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { ctx.ShaExistCache = make(map[string]bool) } for node != nil && node != next && start < len(node.Data) { - m := hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:]) + m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:]) if m == nil { return } diff --git a/modules/markup/html_email.go b/modules/markup/html_email.go index a062789b35..cbfae8b829 100644 --- a/modules/markup/html_email.go +++ b/modules/markup/html_email.go @@ -9,13 +9,13 @@ import "golang.org/x/net/html" func emailAddressProcessor(ctx *RenderContext, node *html.Node) { next := node.NextSibling for node != nil && node != next { - m := emailRegex.FindStringSubmatchIndex(node.Data) + m := globalVars().emailRegex.FindStringSubmatchIndex(node.Data) if m == nil { return } mail := node.Data[m[2]:m[3]] - replaceContent(node, m[2], m[3], createLink("mailto:"+mail, mail, "mailto")) + replaceContent(node, m[2], m[3], createLink(ctx, "mailto:"+mail, mail, "" /*mailto*/)) node = node.NextSibling.NextSibling } } diff --git a/modules/markup/html_emoji.go b/modules/markup/html_emoji.go index c60d06b823..c638065425 100644 --- a/modules/markup/html_emoji.go +++ b/modules/markup/html_emoji.go @@ -13,15 +13,13 @@ import ( "golang.org/x/net/html/atom" ) -func createEmoji(content, class, name string) *html.Node { +func createEmoji(ctx *RenderContext, content, name string) *html.Node { span := &html.Node{ Type: html.ElementNode, Data: atom.Span.String(), Attr: []html.Attribute{}, } - if class != "" { - span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: class}) - } + span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", "emoji")) if name != "" { span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: name}) } @@ -35,13 +33,13 @@ func createEmoji(content, class, name string) *html.Node { return span } -func createCustomEmoji(alias string) *html.Node { +func createCustomEmoji(ctx *RenderContext, alias string) *html.Node { span := &html.Node{ Type: html.ElementNode, Data: atom.Span.String(), Attr: []html.Attribute{}, } - span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: "emoji"}) + span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", "emoji")) span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias}) img := &html.Node{ @@ -62,7 +60,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { start := 0 next := node.NextSibling for node != nil && node != next && start < len(node.Data) { - m := emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:]) + m := globalVars().emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:]) if m == nil { return } @@ -77,7 +75,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { if converted == nil { // check if this is a custom reaction if _, exist := setting.UI.CustomEmojisMap[alias]; exist { - replaceContent(node, m[0], m[1], createCustomEmoji(alias)) + replaceContent(node, m[0], m[1], createCustomEmoji(ctx, alias)) node = node.NextSibling.NextSibling start = 0 continue @@ -85,7 +83,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { continue } - replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description)) + replaceContent(node, m[0], m[1], createEmoji(ctx, converted.Emoji, converted.Description)) node = node.NextSibling.NextSibling start = 0 } @@ -107,7 +105,7 @@ func emojiProcessor(ctx *RenderContext, node *html.Node) { start = m[1] val := emoji.FromCode(codepoint) if val != nil { - replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description)) + replaceContent(node, m[0], m[1], createEmoji(ctx, codepoint, val.Description)) node = node.NextSibling.NextSibling start = 0 } diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 2fb657f56b..cdcc94d563 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -40,17 +40,19 @@ func link(href, class, contents string) string { } var numericMetas = map[string]string{ - "format": "https://someurl.com/{user}/{repo}/{index}", - "user": "someUser", - "repo": "someRepo", - "style": IssueNameStyleNumeric, + "format": "https://someurl.com/{user}/{repo}/{index}", + "user": "someUser", + "repo": "someRepo", + "style": IssueNameStyleNumeric, + "markupAllowShortIssuePattern": "true", } var alphanumericMetas = map[string]string{ - "format": "https://someurl.com/{user}/{repo}/{index}", - "user": "someUser", - "repo": "someRepo", - "style": IssueNameStyleAlphanumeric, + "format": "https://someurl.com/{user}/{repo}/{index}", + "user": "someUser", + "repo": "someRepo", + "style": IssueNameStyleAlphanumeric, + "markupAllowShortIssuePattern": "true", } var regexpMetas = map[string]string{ @@ -62,8 +64,15 @@ var regexpMetas = map[string]string{ // these values should match the TestOrgRepo const above var localMetas = map[string]string{ - "user": "test-owner", - "repo": "test-repo", + "user": "test-owner", + "repo": "test-repo", + "markupAllowShortIssuePattern": "true", +} + +var localWikiMetas = map[string]string{ + "user": "test-owner", + "repo": "test-repo", + "markupContentMode": "wiki", } func TestRender_IssueIndexPattern(t *testing.T) { @@ -124,9 +133,8 @@ func TestRender_IssueIndexPattern2(t *testing.T) { } expectedNil := fmt.Sprintf(expectedFmt, links...) testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{ - Ctx: git.DefaultContext, - Metas: localMetas, - ContentMode: RenderContentAsComment, + Ctx: git.DefaultContext, + Metas: localMetas, }) class := "ref-issue" @@ -139,9 +147,8 @@ func TestRender_IssueIndexPattern2(t *testing.T) { } expectedNum := fmt.Sprintf(expectedFmt, links...) testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{ - Ctx: git.DefaultContext, - Metas: numericMetas, - ContentMode: RenderContentAsComment, + Ctx: git.DefaultContext, + Metas: numericMetas, }) } @@ -262,7 +269,7 @@ func TestRender_IssueIndexPattern5(t *testing.T) { }) } -func TestRender_IssueIndexPattern_Document(t *testing.T) { +func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) { setting.AppURL = TestAppURL metas := map[string]string{ "format": "https://someurl.com/{user}/{repo}/{index}", @@ -285,6 +292,22 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) { }) } +func TestRender_RenderIssueTitle(t *testing.T) { + setting.AppURL = TestAppURL + metas := map[string]string{ + "format": "https://someurl.com/{user}/{repo}/{index}", + "user": "someUser", + "repo": "someRepo", + "style": IssueNameStyleNumeric, + } + actual, err := RenderIssueTitle(&RenderContext{ + Ctx: git.DefaultContext, + Metas: metas, + }, "#1") + assert.NoError(t, err) + assert.Equal(t, "#1", actual) +} + func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { ctx.Links.AbsolutePrefix = true if ctx.Links.Base == "" { @@ -318,8 +341,7 @@ func TestRender_AutoLink(t *testing.T) { Links: Links{ Base: TestRepoURL, }, - Metas: localMetas, - ContentMode: RenderContentAsWiki, + Metas: localWikiMetas, }, strings.NewReader(input), &buffer) assert.Equal(t, err, nil) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) @@ -391,10 +413,10 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) { } for _, testCase := range trueTestCases { - assert.True(t, hashCurrentPattern.MatchString(testCase)) + assert.True(t, globalVars().hashCurrentPattern.MatchString(testCase)) } for _, testCase := range falseTestCases { - assert.False(t, hashCurrentPattern.MatchString(testCase)) + assert.False(t, globalVars().hashCurrentPattern.MatchString(testCase)) } } @@ -474,9 +496,9 @@ func TestRegExp_shortLinkPattern(t *testing.T) { } for _, testCase := range trueTestCases { - assert.True(t, shortLinkPattern.MatchString(testCase)) + assert.True(t, globalVars().shortLinkPattern.MatchString(testCase)) } for _, testCase := range falseTestCases { - assert.False(t, shortLinkPattern.MatchString(testCase)) + assert.False(t, globalVars().shortLinkPattern.MatchString(testCase)) } } diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go index fa630656ce..7341af7eb6 100644 --- a/modules/markup/html_issue.go +++ b/modules/markup/html_issue.go @@ -7,6 +7,7 @@ import ( "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" "code.gitea.io/gitea/modules/regexplru" @@ -23,18 +24,21 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { } next := node.NextSibling for node != nil && node != next { - m := getIssueFullPattern().FindStringSubmatchIndex(node.Data) + m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data) if m == nil { return } - mDiffView := getFilesChangedFullPattern().FindStringSubmatchIndex(node.Data) + mDiffView := globalVars().filesChangedFullPattern.FindStringSubmatchIndex(node.Data) // leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files if mDiffView != nil { return } link := node.Data[m[0]:m[1]] + if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, link) { + return + } text := "#" + node.Data[m[2]:m[3]] // if m[4] and m[5] is not -1, then link is to a comment // indicate that in the text by appending (comment) @@ -53,10 +57,10 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { matchRepo := linkParts[len(linkParts)-3] if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] { - replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue")) + replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue")) } else { text = matchOrg + "/" + matchRepo + text - replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue")) + replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue")) } node = node.NextSibling.NextSibling } @@ -67,8 +71,10 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { return } - // crossLinkOnly if not comment and not wiki - crossLinkOnly := ctx.ContentMode != RenderContentAsTitle && ctx.ContentMode != RenderContentAsComment && ctx.ContentMode != RenderContentAsWiki + // crossLinkOnly: do not parse "#123", only parse "owner/repo#123" + // if there is no repo in the context, then the "#123" format can't be parsed + // old logic: crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki + crossLinkOnly := ctx.Metas["markupAllowShortIssuePattern"] != "true" var ( found bool @@ -123,16 +129,16 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err) } - link = createLink(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. issuePath := util.Iif(ref.IsPull, "pulls", "issues") if ref.Owner == "" { - link = createLink(util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue") + link = createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue") } else { - link = createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue") + link = createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue") } } @@ -145,7 +151,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { // Decorate action keywords if actionable var keyword *html.Node if references.IsXrefActionable(ref, hasExtTrackFormat) { - keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End]) + keyword = createKeyword(ctx, node.Data[ref.ActionLocation.Start:ref.ActionLocation.End]) } else { keyword = &html.Node{ Type: html.TextNode, @@ -171,7 +177,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { } reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) - link := createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit") + link := createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit") replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) node = node.NextSibling.NextSibling diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go index 30564da548..32aa7dc614 100644 --- a/modules/markup/html_link.go +++ b/modules/markup/html_link.go @@ -20,9 +20,9 @@ func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (resu isAnchorFragment := link != "" && link[0] == '#' if !isAnchorFragment && !IsFullURLString(link) { linkBase := ctx.Links.Base - if ctx.ContentMode == RenderContentAsWiki { + if ctx.IsMarkupContentWiki() { // no need to check if the link should be resolved as a wiki link or a wiki raw link - // just use wiki link here and it will be redirected to a wiki raw link if necessary + // just use wiki link here, and it will be redirected to a wiki raw link if necessary linkBase = ctx.Links.WikiLink() } else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" { // if there is no BranchPath, then the link will be something like "/owner/repo/src/{the-file-path}" @@ -40,7 +40,7 @@ func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (resu func shortLinkProcessor(ctx *RenderContext, node *html.Node) { next := node.NextSibling for node != nil && node != next { - m := shortLinkPattern.FindStringSubmatchIndex(node.Data) + m := globalVars().shortLinkPattern.FindStringSubmatchIndex(node.Data) if m == nil { return } @@ -147,7 +147,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { } if image { if !absoluteLink { - link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), link) + link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link) } title := props["title"] if title == "" { @@ -189,41 +189,22 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { func linkProcessor(ctx *RenderContext, node *html.Node) { next := node.NextSibling for node != nil && node != next { - m := common.LinkRegex.FindStringIndex(node.Data) + m := common.GlobalVars().LinkRegex.FindStringIndex(node.Data) if m == nil { return } uri := node.Data[m[0]:m[1]] - replaceContent(node, m[0], m[1], createLink(uri, uri, "link")) + replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/)) node = node.NextSibling.NextSibling } } -func genDefaultLinkProcessor(defaultLink string) processor { - return func(ctx *RenderContext, node *html.Node) { - ch := &html.Node{ - Parent: node, - Type: html.TextNode, - Data: node.Data, - } - - node.Type = html.ElementNode - node.Data = "a" - node.DataAtom = atom.A - node.Attr = []html.Attribute{ - {Key: "href", Val: defaultLink}, - {Key: "class", Val: "default-link muted"}, - } - node.FirstChild, node.LastChild = ch, ch - } -} - // descriptionLinkProcessor creates links for DescriptionHTML func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) { next := node.NextSibling for node != nil && node != next { - m := common.LinkRegex.FindStringIndex(node.Data) + m := common.GlobalVars().LinkRegex.FindStringIndex(node.Data) if m == nil { return } diff --git a/modules/markup/html_mention.go b/modules/markup/html_mention.go index 3f0692e05f..f7e2ad50f1 100644 --- a/modules/markup/html_mention.go +++ b/modules/markup/html_mention.go @@ -33,7 +33,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { if ok && strings.Contains(mention, "/") { mentionOrgAndTeam := strings.Split(mention, "/") if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { - replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention")) + replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "" /*mention*/)) node = node.NextSibling.NextSibling start = 0 continue @@ -44,7 +44,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { mentionedUsername := mention[1:] if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) { - replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "mention")) + replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "" /*mention*/)) node = node.NextSibling.NextSibling start = 0 } else { diff --git a/modules/markup/html_node.go b/modules/markup/html_node.go index c499854053..234adba2bf 100644 --- a/modules/markup/html_node.go +++ b/modules/markup/html_node.go @@ -17,7 +17,7 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { } if IsNonEmptyRelativePath(attr.Val) { - attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), attr.Val) + attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val) // By default, the "" tag should also be clickable, // because frontend use `` to paste the re-scaled image into the markdown, @@ -53,7 +53,7 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) { continue } if IsNonEmptyRelativePath(attr.Val) { - attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), attr.Val) + attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val) } attr.Val = camoHandleLink(attr.Val) node.Attr[i] = attr diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 262d0fc4dd..67ac2758a3 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -27,6 +27,11 @@ var ( "user": testRepoOwnerName, "repo": testRepoName, } + localWikiMetas = map[string]string{ + "user": testRepoOwnerName, + "repo": testRepoName, + "markupContentMode": "wiki", + } ) type mockRepo struct { @@ -413,8 +418,7 @@ func TestRender_ShortLinks(t *testing.T) { Links: markup.Links{ Base: markup.TestRepoURL, }, - Metas: localMetas, - ContentMode: markup.RenderContentAsWiki, + Metas: localWikiMetas, }, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) @@ -526,10 +530,9 @@ func TestRender_ShortLinks(t *testing.T) { func TestRender_RelativeMedias(t *testing.T) { render := func(input string, isWiki bool, links markup.Links) string { buffer, err := markdown.RenderString(&markup.RenderContext{ - Ctx: git.DefaultContext, - Links: links, - Metas: localMetas, - ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsComment), + Ctx: git.DefaultContext, + Links: links, + Metas: util.Iif(isWiki, localWikiMetas, localMetas), }, input) assert.NoError(t, err) return strings.TrimSpace(string(buffer)) diff --git a/modules/markup/internal/finalprocessor.go b/modules/markup/internal/finalprocessor.go new file mode 100644 index 0000000000..14d46a161f --- /dev/null +++ b/modules/markup/internal/finalprocessor.go @@ -0,0 +1,30 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "bytes" + "io" +) + +type finalProcessor struct { + renderInternal *RenderInternal + + output io.Writer + buf bytes.Buffer +} + +func (p *finalProcessor) Write(data []byte) (int, error) { + p.buf.Write(data) + return len(data), nil +} + +func (p *finalProcessor) Close() error { + // TODO: reading the whole markdown isn't a problem at the moment, + // because "postProcess" already does so. In the future we could optimize the code to process data on the fly. + buf := p.buf.Bytes() + buf = bytes.ReplaceAll(buf, []byte(` data-attr-class="`+p.renderInternal.secureIDPrefix), []byte(` class="`)) + _, err := p.output.Write(buf) + return err +} diff --git a/modules/markup/internal/internal_test.go b/modules/markup/internal/internal_test.go new file mode 100644 index 0000000000..98ff3bc079 --- /dev/null +++ b/modules/markup/internal/internal_test.go @@ -0,0 +1,61 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "bytes" + "html/template" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRenderInternal(t *testing.T) { + cases := []struct { + input, protected, recovered string + }{ + { + input: ``)
- if err != nil {
- return
- }
-
- // include language-x class as part of commonmark spec
- // the "display" class is used by "js/markup/math.js" to render the code element as a block
- _, err = w.WriteString(``)
- if err != nil {
- return
- }
- } else {
- _, err := w.WriteString("
")
- if err != nil {
- return
- }
- }
- }),
- ),
- math.NewExtension(
- math.Enabled(setting.Markdown.EnableMath),
- ),
- meta.Meta,
- ),
- goldmark.WithParserOptions(
- parser.WithAttribute(),
- parser.WithAutoHeadingID(),
- parser.WithASTTransformers(
- util.Prioritized(NewASTTransformer(), 10000),
- ),
- ),
- goldmark.WithRendererOptions(
- html.WithUnsafe(),
- ),
- )
-
- // Override the original Tasklist renderer!
- specMarkdown.Renderer().AddOptions(
- renderer.WithNodeRenderers(
- util.Prioritized(NewHTMLRenderer(), 10),
- ),
- )
- })
- return specMarkdown
+ goldmarkMarkdown goldmark.Markdown
}
-// actualRender renders Markdown to HTML without handling special links.
-func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
- converter := SpecializedMarkdown()
+func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error {
+ return r.goldmarkMarkdown.Convert(source, writer, opts...)
+}
+
+func (r *GlodmarkRender) Renderer() renderer.Renderer {
+ return r.goldmarkMarkdown.Renderer()
+}
+
+func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
+ if entering {
+ language, _ := c.Language()
+ if language == nil {
+ language = []byte("text")
+ }
+
+ languageStr := string(language)
+
+ preClasses := []string{"code-block"}
+ if languageStr == "mermaid" || languageStr == "math" {
+ preClasses = append(preClasses, "is-loading")
+ }
+
+ err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``, strings.Join(preClasses, " "))
+ if err != nil {
+ return
+ }
+
+ // include language-x class as part of commonmark spec
+ // the "display" class is used by "js/markup/math.js" to render the code element as a block
+ err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``, string(language))
+ if err != nil {
+ return
+ }
+ } else {
+ _, err := w.WriteString("
")
+ if err != nil {
+ return
+ }
+ }
+}
+
+// SpecializedMarkdown sets up the Gitea specific markdown extensions
+func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
+ // TODO: it could use a pool to cache the renderers to reuse them with different contexts
+ // at the moment it is fast enough (see the benchmarks)
+ r := &GlodmarkRender{ctx: ctx}
+ r.goldmarkMarkdown = goldmark.New(
+ goldmark.WithExtensions(
+ extension.NewTable(extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
+ extension.Strikethrough,
+ extension.TaskList,
+ extension.DefinitionList,
+ common.FootnoteExtension,
+ highlighting.NewHighlighting(
+ highlighting.WithFormatOptions(
+ chromahtml.WithClasses(true),
+ chromahtml.PreventSurroundingPre(true),
+ ),
+ highlighting.WithWrapperRenderer(r.highlightingRenderer),
+ ),
+ math.NewExtension(&ctx.RenderInternal, math.Enabled(setting.Markdown.EnableMath)),
+ meta.Meta,
+ ),
+ goldmark.WithParserOptions(
+ parser.WithAttribute(),
+ parser.WithAutoHeadingID(),
+ parser.WithASTTransformers(util.Prioritized(NewASTTransformer(&ctx.RenderInternal), 10000)),
+ ),
+ goldmark.WithRendererOptions(html.WithUnsafe()),
+ )
+
+ // Override the original Tasklist renderer!
+ r.goldmarkMarkdown.Renderer().AddOptions(
+ renderer.WithNodeRenderers(util.Prioritized(NewHTMLRenderer(&ctx.RenderInternal), 10)),
+ )
+
+ return r
+}
+
+// render calls goldmark render to convert Markdown to HTML
+// NOTE: The output of this method MUST get sanitized separately!!!
+func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
+ converter := SpecializedMarkdown(ctx)
lw := &limitWriter{
w: output,
limit: setting.UI.MaxDisplayFileSize * 3,
@@ -160,8 +164,8 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
}
log.Warn("Unable to render markdown due to panic in goldmark: %v", err)
- if log.IsDebug() {
- log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2))
+ if (!setting.IsProd && !setting.IsInTesting) || log.IsDebug() {
+ log.Error("Panic in markdown: %v\n%s", err, log.Stack(2))
}
}()
@@ -200,26 +204,6 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
return nil
}
-// Note: The output of this method must get sanitized.
-func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
- defer func() {
- err := recover()
- if err == nil {
- return
- }
-
- log.Warn("Unable to render markdown due to panic in goldmark - will return raw bytes")
- if log.IsDebug() {
- log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2))
- }
- _, err = io.Copy(output, input)
- if err != nil {
- log.Error("io.Copy failed: %v", err)
- }
- }()
- return actualRender(ctx, input, output)
-}
-
// MarkupName describes markup's name
var MarkupName = "markdown"
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index 315eed2e62..e4889a75e5 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -37,6 +37,12 @@ var localMetas = map[string]string{
"repo": testRepoName,
}
+var localWikiMetas = map[string]string{
+ "user": testRepoOwnerName,
+ "repo": testRepoName,
+ "markupContentMode": "wiki",
+}
+
type mockRepo struct {
OwnerName string
RepoName string
@@ -75,7 +81,7 @@ func TestRender_StandardLinks(t *testing.T) {
Links: markup.Links{
Base: FullURL,
},
- ContentMode: markup.RenderContentAsWiki,
+ Metas: localWikiMetas,
}, input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
@@ -307,9 +313,8 @@ func TestTotal_RenderWiki(t *testing.T) {
Links: markup.Links{
Base: FullURL,
},
- Repo: newMockRepo(testRepoOwnerName, testRepoName),
- Metas: localMetas,
- ContentMode: markup.RenderContentAsWiki,
+ Repo: newMockRepo(testRepoOwnerName, testRepoName),
+ Metas: localWikiMetas,
}, sameCases[i])
assert.NoError(t, err)
assert.Equal(t, answers[i], string(line))
@@ -334,7 +339,7 @@ func TestTotal_RenderWiki(t *testing.T) {
Links: markup.Links{
Base: FullURL,
},
- ContentMode: markup.RenderContentAsWiki,
+ Metas: localWikiMetas,
}, testCases[i])
assert.NoError(t, err)
assert.EqualValues(t, testCases[i+1], string(line))
@@ -657,9 +662,9 @@ mail@domain.com
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
`)
+ _ = r.renderInternal.FormatWithSafeAttrs(w, ``)
r.writeLines(w, source, n)
} else {
_, _ = w.WriteString(`
` + "\n")
diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go
index 96848099cc..0cff4f1e74 100644
--- a/modules/markup/markdown/math/inline_renderer.go
+++ b/modules/markup/markdown/math/inline_renderer.go
@@ -6,17 +6,21 @@ package math
import (
"bytes"
+ "code.gitea.io/gitea/modules/markup/internal"
+
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
// InlineRenderer is an inline renderer
-type InlineRenderer struct{}
+type InlineRenderer struct {
+ renderInternal *internal.RenderInternal
+}
// NewInlineRenderer returns a new renderer for inline math
-func NewInlineRenderer() renderer.NodeRenderer {
- return &InlineRenderer{}
+func NewInlineRenderer(renderInternal *internal.RenderInternal) renderer.NodeRenderer {
+ return &InlineRenderer{renderInternal: renderInternal}
}
func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -25,7 +29,7 @@ func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Nod
if _, ok := n.(*InlineBlock); ok {
extraClass = "display "
}
- _, _ = w.WriteString(``)
+ _ = r.renderInternal.FormatWithSafeAttrs(w, ``, extraClass)
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
segment := c.(*ast.Text).Segment
value := util.EscapeHTML(segment.Value(source))
diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go
index 3d9f376bc6..7e8defcd4a 100644
--- a/modules/markup/markdown/math/math.go
+++ b/modules/markup/markdown/math/math.go
@@ -4,6 +4,8 @@
package math
import (
+ "code.gitea.io/gitea/modules/markup/internal"
+
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
@@ -12,6 +14,7 @@ import (
// Extension is a math extension
type Extension struct {
+ renderInternal *internal.RenderInternal
enabled bool
parseDollarInline bool
parseDollarBlock bool
@@ -39,38 +42,10 @@ func Enabled(enable ...bool) Option {
})
}
-// WithInlineDollarParser enables or disables the parsing of $...$
-func WithInlineDollarParser(enable ...bool) Option {
- value := true
- if len(enable) > 0 {
- value = enable[0]
- }
- return extensionFunc(func(e *Extension) {
- e.parseDollarInline = value
- })
-}
-
-// WithBlockDollarParser enables or disables the parsing of $$...$$
-func WithBlockDollarParser(enable ...bool) Option {
- value := true
- if len(enable) > 0 {
- value = enable[0]
- }
- return extensionFunc(func(e *Extension) {
- e.parseDollarBlock = value
- })
-}
-
-// Math represents a math extension with default rendered delimiters
-var Math = &Extension{
- enabled: true,
- parseDollarBlock: true,
- parseDollarInline: true,
-}
-
// NewExtension creates a new math extension with the provided options
-func NewExtension(opts ...Option) *Extension {
+func NewExtension(renderInternal *internal.RenderInternal, opts ...Option) *Extension {
r := &Extension{
+ renderInternal: renderInternal,
enabled: true,
parseDollarBlock: true,
parseDollarInline: true,
@@ -102,7 +77,7 @@ func (e *Extension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithInlineParsers(inlines...))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
- util.Prioritized(NewBlockRenderer(), 501),
- util.Prioritized(NewInlineRenderer(), 502),
+ util.Prioritized(NewBlockRenderer(e.renderInternal), 501),
+ util.Prioritized(NewInlineRenderer(e.renderInternal), 502),
))
}
diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go
index 6949966328..278c33f1d2 100644
--- a/modules/markup/markdown/meta_test.go
+++ b/modules/markup/markdown/meta_test.go
@@ -11,10 +11,8 @@ import (
"github.com/stretchr/testify/assert"
)
-/*
-IssueTemplate is a legacy to keep the unit tests working.
-Copied from structs.IssueTemplate, the original type has been changed a lot to support yaml template.
-*/
+// IssueTemplate is a legacy to keep the unit tests working.
+// Copied from structs.IssueTemplate, the original type has been changed a lot to support yaml template.
type IssueTemplate struct {
Name string `json:"name" yaml:"name"`
Title string `json:"title" yaml:"title"`
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
index 92dc500e69..2651d44a69 100644
--- a/modules/markup/markdown/transform_blockquote.go
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -32,7 +32,8 @@ func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast
default: // including "note"
octiconName = "info"
}
- _, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
+ svgHTML := svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)
+ _, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(svgHTML)))
}
return ast.WalkContinue, nil
}
@@ -128,13 +129,13 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
}
// color the blockquote
- v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
+ v.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("attention-header attention-"+attentionType)))
// create an emphasis to make it bold
attentionParagraph := ast.NewParagraph()
g.applyElementDir(attentionParagraph)
emphasis := ast.NewEmphasis(2)
- emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
+ emphasis.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("attention-"+attentionType)))
attentionAstString := ast.NewString([]byte(cases.Title(language.English).String(attentionType)))
diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go
index ff7d24eec9..bccc43aad2 100644
--- a/modules/markup/markdown/transform_codespan.go
+++ b/modules/markup/markdown/transform_codespan.go
@@ -5,7 +5,6 @@ package markdown
import (
"bytes"
- "fmt"
"strings"
"code.gitea.io/gitea/modules/markup"
@@ -40,7 +39,7 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
r.Writer.RawWrite(w, value)
}
case *ColorPreview:
- _, _ = w.WriteString(fmt.Sprintf(``, string(v.Color)))
+ _ = r.renderInternal.FormatWithSafeAttrs(w, ``, string(v.Color))
}
}
return ast.WalkSkipChildren, nil
diff --git a/modules/markup/markdown/transform_image.go b/modules/markup/markdown/transform_image.go
index 4ed4118854..b2262c1c78 100644
--- a/modules/markup/markdown/transform_image.go
+++ b/modules/markup/markdown/transform_image.go
@@ -21,7 +21,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image)
// Check if the destination is a real link
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
v.Destination = []byte(giteautil.URLJoin(
- ctx.Links.ResolveMediaLink(ctx.ContentMode == markup.RenderContentAsWiki),
+ ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()),
strings.TrimLeft(string(v.Destination), "/"),
))
}
diff --git a/modules/markup/markdown/transform_list.go b/modules/markup/markdown/transform_list.go
index b982fd4a83..c89ad2f2cf 100644
--- a/modules/markup/markdown/transform_list.go
+++ b/modules/markup/markdown/transform_list.go
@@ -72,7 +72,7 @@ func (g *ASTTransformer) transformList(_ *markup.RenderContext, v *ast.List, rc
}
newChild := NewTaskCheckBoxListItem(listItem)
newChild.IsChecked = taskCheckBox.IsChecked
- newChild.SetAttributeString("class", []byte("task-list-item"))
+ newChild.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("task-list-item")))
segments := newChild.FirstChild().Lines()
if segments.Len() > 0 {
segment := segments.At(0)
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index 6b9c963157..c587a6ada5 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -144,15 +144,14 @@ func (r *Writer) resolveLink(kind, link string) string {
}
base := r.Ctx.Links.Base
- isWiki := r.Ctx.ContentMode == markup.RenderContentAsWiki
- if isWiki {
+ if r.Ctx.IsMarkupContentWiki() {
base = r.Ctx.Links.WikiLink()
} else if r.Ctx.Links.HasBranchInfo() {
base = r.Ctx.Links.SrcLink()
}
if kind == "image" || kind == "video" {
- base = r.Ctx.Links.ResolveMediaLink(isWiki)
+ base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki())
}
link = util.URLJoin(base, link)
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index b882678c7e..a3eefc3db3 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -27,7 +27,7 @@ func TestRender_StandardLinks(t *testing.T) {
Base: "/relative-path",
BranchPath: "branch/main",
},
- ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsDefault),
+ Metas: map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")},
}, input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
diff --git a/modules/markup/render.go b/modules/markup/render.go
index add50f4382..f05cb62626 100644
--- a/modules/markup/render.go
+++ b/modules/markup/render.go
@@ -9,14 +9,15 @@ import (
"io"
"net/url"
"strings"
- "sync"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/markup/internal"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast"
+ "golang.org/x/sync/errgroup"
)
type RenderMetaMode string
@@ -27,15 +28,6 @@ const (
RenderMetaAsTable RenderMetaMode = "table"
)
-type RenderContentMode string
-
-const (
- RenderContentAsDefault RenderContentMode = "" // empty means "default", no special handling, maybe just a simple "document"
- RenderContentAsComment RenderContentMode = "comment"
- RenderContentAsTitle RenderContentMode = "title"
- RenderContentAsWiki RenderContentMode = "wiki"
-)
-
var RenderBehaviorForTesting struct {
// Markdown line break rendering has 2 default behaviors:
// * Use hard: replace "\n" with "
" for comments, setting.Markdown.EnableHardLineBreakInComments=true
@@ -59,12 +51,14 @@ type RenderContext struct {
// for file mode, it could be left as empty, and will be detected by file extension in RelativePath
MarkupType string
- // what the content will be used for: eg: for comment or for wiki? or just render a file?
- ContentMode RenderContentMode
+ Links Links // special link references for rendering, especially when there is a branch/tree path
+
+ // user&repo, format&style®exp (for external issue pattern), teams&org (for mention)
+ // BranchNameSubURL (for iframe&asciicast)
+ // markupAllowShortIssuePattern, markupContentMode (wiki)
+ // markdownLineBreakStyle (comment, document)
+ Metas map[string]string
- Links Links // special link references for rendering, especially when there is a branch/tree path
- Metas map[string]string // user&repo, format&style®exp (for external issue pattern), teams&org (for mention), BranchNameSubURL(for iframe&asciicast)
- DefaultLink string // TODO: need to figure out
GitRepo *git.Repository
Repo gitrepo.Repository
ShaExistCache map[string]bool
@@ -72,6 +66,8 @@ type RenderContext struct {
SidebarTocNode ast.Node
RenderMetaAs RenderMetaMode
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
+
+ RenderInternal internal.RenderInternal
}
// Cancel runs any cleanup functions that have been registered for this Ctx
@@ -102,6 +98,10 @@ func (ctx *RenderContext) AddCancel(fn func()) {
}
}
+func (ctx *RenderContext) IsMarkupContentWiki() bool {
+ return ctx.Metas != nil && ctx.Metas["markupContentMode"] == "wiki"
+}
+
// Render renders markup file to HTML with all specific handling stuff.
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
if ctx.MarkupType == "" && ctx.RelativePath != "" {
@@ -159,59 +159,53 @@ sandbox="allow-scripts"
return err
}
-func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
- var wg sync.WaitGroup
- var err error
+func pipes() (io.ReadCloser, io.WriteCloser, func()) {
pr, pw := io.Pipe()
- defer func() {
+ return pr, pw, func() {
_ = pr.Close()
_ = pw.Close()
- }()
+ }
+}
- var pr2 io.ReadCloser
- var pw2 io.WriteCloser
+func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
+ finalProcessor := ctx.RenderInternal.Init(output)
+ defer finalProcessor.Close()
- var sanitizerDisabled bool
- if r, ok := renderer.(ExternalRenderer); ok {
- sanitizerDisabled = r.SanitizerDisabled()
+ // input -> (pw1=pr1) -> renderer -> (pw2=pr2) -> SanitizeReader -> finalProcessor -> output
+ // no sanitizer: input -> (pw1=pr1) -> renderer -> pw2(finalProcessor) -> output
+ pr1, pw1, close1 := pipes()
+ defer close1()
+
+ eg, _ := errgroup.WithContext(ctx.Ctx)
+ var pw2 io.WriteCloser = util.NopCloser{Writer: finalProcessor}
+
+ if r, ok := renderer.(ExternalRenderer); !ok || !r.SanitizerDisabled() {
+ var pr2 io.ReadCloser
+ var close2 func()
+ pr2, pw2, close2 = pipes()
+ defer close2()
+ eg.Go(func() error {
+ defer pr2.Close()
+ return SanitizeReader(pr2, renderer.Name(), finalProcessor)
+ })
}
- if !sanitizerDisabled {
- pr2, pw2 = io.Pipe()
- defer func() {
- _ = pr2.Close()
- _ = pw2.Close()
- }()
-
- wg.Add(1)
- go func() {
- err = SanitizeReader(pr2, renderer.Name(), output)
- _ = pr2.Close()
- wg.Done()
- }()
- } else {
- pw2 = util.NopCloser{Writer: output}
- }
-
- wg.Add(1)
- go func() {
+ eg.Go(func() (err error) {
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
- err = PostProcess(ctx, pr, pw2)
+ err = PostProcess(ctx, pr1, pw2)
} else {
- _, err = io.Copy(pw2, pr)
+ _, err = io.Copy(pw2, pr1)
}
- _ = pr.Close()
- _ = pw2.Close()
- wg.Done()
- }()
+ _, _ = pr1.Close(), pw2.Close()
+ return err
+ })
- if err1 := renderer.Render(ctx, input, pw); err1 != nil {
- return err1
+ if err := renderer.Render(ctx, input, pw1); err != nil {
+ return err
}
- _ = pw.Close()
+ _ = pw1.Close()
- wg.Wait()
- return err
+ return eg.Wait()
}
// Init initializes the render global variables
@@ -232,3 +226,7 @@ func Init(ph *ProcessorHelper) {
}
}
}
+
+func ComposeSimpleDocumentMetas() map[string]string {
+ return map[string]string{"markdownLineBreakStyle": "document"}
+}
diff --git a/modules/markup/render_links.go b/modules/markup/render_links.go
index 3e1aa7ce3a..c8339d8f8b 100644
--- a/modules/markup/render_links.go
+++ b/modules/markup/render_links.go
@@ -10,7 +10,7 @@ import (
type Links struct {
AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
- Base string // base prefix for pre-provided links and medias (images, videos)
+ Base string // base prefix for pre-provided links and medias (images, videos), usually it is the path to the repo
BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
}
diff --git a/modules/markup/sanitizer_custom.go b/modules/markup/sanitizer_custom.go
index 7978973166..7f96556fd7 100644
--- a/modules/markup/sanitizer_custom.go
+++ b/modules/markup/sanitizer_custom.go
@@ -4,6 +4,9 @@
package markup
import (
+ "regexp"
+ "strings"
+
"code.gitea.io/gitea/modules/setting"
"github.com/microcosm-cc/bluemonday"
@@ -15,8 +18,11 @@ func (st *Sanitizer) addSanitizerRules(policy *bluemonday.Policy, rules []settin
policy.AllowDataURIImages()
}
if rule.Element != "" {
- if rule.Regexp != nil {
- policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
+ if rule.Regexp != "" {
+ if !strings.HasPrefix(rule.Regexp, "^") || !strings.HasSuffix(rule.Regexp, "$") {
+ panic("Markup sanitizer rule regexp must start with ^ and end with $ to be strict")
+ }
+ policy.AllowAttrs(rule.AllowAttr).Matching(regexp.MustCompile(rule.Regexp)).OnElements(rule.Element)
} else {
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
}
diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go
index 476ae5e26f..0fa54efd45 100644
--- a/modules/markup/sanitizer_default.go
+++ b/modules/markup/sanitizer_default.go
@@ -16,37 +16,12 @@ import (
func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
policy := bluemonday.UGCPolicy()
- // For JS code copy and Mermaid loading state
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
+ // NOTICE: DO NOT add special "class" regexp rules here anymore, use RenderInternal.SafeAttr instead
- // For code preview
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
- policy.AllowAttrs("data-line-number").OnElements("span")
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
-
- // For code preview (unicode escape)
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
- policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
-
- // For color preview
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
-
- // For attention
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
- policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
+ // General safe SVG attributes
+ policy.AllowAttrs("viewBox", "width", "height", "aria-hidden", "data-attr-class").OnElements("svg")
policy.AllowAttrs("fill-rule", "d").OnElements("path")
- // For Chroma markdown plugin
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
-
// Checkboxes
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
@@ -66,28 +41,15 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
}
- // Allow classes for anchors
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
-
- // Allow classes for task lists
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
-
// Allow classes for org mode list item status.
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
- // Allow icons
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
-
- // Allow classes for emojis
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
-
- // Allow icons, emojis, chroma syntax and keyword markup on span
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
-
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
policy.AllowStyles("color", "background-color").OnElements("span", "p")
- // Allow generally safe attributes
+ policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
+
+ // Allow generally safe attributes (reference: https://github.com/jch/html-pipeline)
generalSafeAttrs := []string{
"abbr", "accept", "accept-charset",
"accesskey", "action", "align", "alt",
@@ -106,10 +68,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
"selected", "shape", "size", "span",
"start", "summary", "tabindex", "target",
"title", "type", "usemap", "valign", "value",
- "vspace", "width", "itemprop",
- "data-markdown-generated-content",
+ "vspace", "width", "itemprop", "itemscope", "itemtype",
+ "data-markdown-generated-content", "data-attr-class",
}
-
generalSafeElements := []string{
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
@@ -117,14 +78,8 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
"details", "caption", "figure", "figcaption",
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
}
-
- policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
-
- policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
-
- policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
-
// FIXME: Need to handle longdesc in img but there is no easy way to do it
+ policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
// Custom keyword markup
defaultSanitizer.addSanitizerRules(policy, setting.ExternalSanitizerRules)
diff --git a/modules/markup/sanitizer_default_test.go b/modules/markup/sanitizer_default_test.go
index 20370509c1..c5c43695ea 100644
--- a/modules/markup/sanitizer_default_test.go
+++ b/modules/markup/sanitizer_default_test.go
@@ -19,7 +19,6 @@ func TestSanitizer(t *testing.T) {
// Code highlighting class
`
`, `
`,
`
`, `
`,
- `
`, `
`,
// Input checkbox
``, ``,
@@ -38,10 +37,8 @@ func TestSanitizer(t *testing.T) {
// tags
`Ctrl + C`, `Ctrl + C`,
`NAUGHTY`, `NAUGHTY`,
- ``, ``,
`unchecked`, `unchecked`,
`NAUGHTY`, `NAUGHTY`,
- `contents`, `contents`,
// Color property
`Hello World`, `Hello World`,
diff --git a/modules/queue/manager.go b/modules/queue/manager.go
index 8b964c0c28..079e2bee7a 100644
--- a/modules/queue/manager.go
+++ b/modules/queue/manager.go
@@ -5,6 +5,7 @@ package queue
import (
"context"
+ "errors"
"sync"
"time"
@@ -32,6 +33,7 @@ type ManagedWorkerPoolQueue interface {
// FlushWithContext tries to make the handler process all items in the queue synchronously.
// It is for testing purpose only. It's not designed to be used in a cluster.
+ // Negative timeout means discarding all items in the queue.
FlushWithContext(ctx context.Context, timeout time.Duration) error
// RemoveAllItems removes all items in the base queue (on-the-fly items are not affected)
@@ -76,15 +78,16 @@ func (m *Manager) ManagedQueues() map[int64]ManagedWorkerPoolQueue {
// FlushAll tries to make all managed queues process all items synchronously, until timeout or the queue is empty.
// It is for testing purpose only. It's not designed to be used in a cluster.
+// Negative timeout means discarding all items in the queue.
func (m *Manager) FlushAll(ctx context.Context, timeout time.Duration) error {
- var finalErr error
+ var finalErrors []error
qs := m.ManagedQueues()
for _, q := range qs {
if err := q.FlushWithContext(ctx, timeout); err != nil {
- finalErr = err // TODO: in Go 1.20: errors.Join
+ finalErrors = append(finalErrors, err)
}
}
- return finalErr
+ return errors.Join(finalErrors...)
}
// CreateSimpleQueue creates a simple queue from global setting config provider by name
diff --git a/modules/queue/workergroup.go b/modules/queue/workergroup.go
index 153123f883..82b0790d5a 100644
--- a/modules/queue/workergroup.go
+++ b/modules/queue/workergroup.go
@@ -23,7 +23,7 @@ var (
)
func init() {
- unhandledItemRequeueDuration.Store(int64(5 * time.Second))
+ unhandledItemRequeueDuration.Store(int64(time.Second))
}
// workerGroup is a group of workers to work with a WorkerPoolQueue
@@ -104,7 +104,12 @@ func (q *WorkerPoolQueue[T]) doWorkerHandle(batch []T) {
// if none of the items were handled, it should back-off for a few seconds
// in this case the handler (eg: document indexer) may have encountered some errors/failures
if len(unhandled) == len(batch) && unhandledItemRequeueDuration.Load() != 0 {
+ if q.isFlushing.Load() {
+ return // do not requeue items when flushing, since all items failed, requeue them will continue failing.
+ }
log.Error("Queue %q failed to handle batch of %d items, backoff for a few seconds", q.GetName(), len(batch))
+ // TODO: ideally it shouldn't "sleep" here (blocks the worker, then blocks flush).
+ // It could debounce the requeue operation, and try to requeue the items in the future.
select {
case <-q.ctxRun.Done():
case <-time.After(time.Duration(unhandledItemRequeueDuration.Load())):
@@ -193,19 +198,37 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
// doFlush flushes the queue: it tries to read all items from the queue and handles them.
// It is for testing purpose only. It's not designed to work for a cluster.
func (q *WorkerPoolQueue[T]) doFlush(wg *workerGroup[T], flush flushType) {
+ q.isFlushing.Store(true)
+ defer q.isFlushing.Store(false)
+
log.Debug("Queue %q starts flushing", q.GetName())
defer log.Debug("Queue %q finishes flushing", q.GetName())
// stop all workers, and prepare a new worker context to start new workers
-
wg.ctxWorkerCancel()
wg.wg.Wait()
defer func() {
- close(flush)
+ close(flush.c)
wg.doPrepareWorkerContext()
}()
+ if flush.timeout < 0 {
+ // discard everything
+ wg.batchBuffer = nil
+ for {
+ select {
+ case <-wg.popItemChan:
+ case <-wg.popItemErr:
+ case <-q.batchChan:
+ case <-q.ctxRun.Done():
+ return
+ default:
+ return
+ }
+ }
+ }
+
// drain the batch channel first
loop:
for {
@@ -221,6 +244,9 @@ loop:
emptyCounter := 0
for {
select {
+ case <-q.ctxRun.Done():
+ log.Debug("Queue %q is shutting down", q.GetName())
+ return
case data, dataOk := <-wg.popItemChan:
if !dataOk {
return
@@ -236,9 +262,6 @@ loop:
log.Error("Failed to pop item from queue %q (doFlush): %v", q.GetName(), err)
}
return
- case <-q.ctxRun.Done():
- log.Debug("Queue %q is shutting down", q.GetName())
- return
case <-time.After(20 * time.Millisecond):
// There is no reliable way to make sure all queue items are consumed by the Flush, there always might be some items stored in some buffers/temp variables.
// If we run Gitea in a cluster, we can even not guarantee all items are consumed in a deterministic instance.
@@ -316,6 +339,15 @@ func (q *WorkerPoolQueue[T]) doRun() {
var batchDispatchC <-chan time.Time = infiniteTimerC
for {
select {
+ case flush := <-q.flushChan:
+ // before flushing, it needs to try to dispatch the batch to worker first, in case there is no worker running
+ // after the flushing, there is at least one worker running, so "doFlush" could wait for workers to finish
+ // since we are already in a "flush" operation, so the dispatching function shouldn't read the flush chan.
+ q.doDispatchBatchToWorker(wg, skipFlushChan)
+ q.doFlush(wg, flush)
+ case <-q.ctxRun.Done():
+ log.Debug("Queue %q is shutting down", q.GetName())
+ return
case data, dataOk := <-wg.popItemChan:
if !dataOk {
return
@@ -334,20 +366,11 @@ func (q *WorkerPoolQueue[T]) doRun() {
case <-batchDispatchC:
batchDispatchC = infiniteTimerC
q.doDispatchBatchToWorker(wg, q.flushChan)
- case flush := <-q.flushChan:
- // before flushing, it needs to try to dispatch the batch to worker first, in case there is no worker running
- // after the flushing, there is at least one worker running, so "doFlush" could wait for workers to finish
- // since we are already in a "flush" operation, so the dispatching function shouldn't read the flush chan.
- q.doDispatchBatchToWorker(wg, skipFlushChan)
- q.doFlush(wg, flush)
case err := <-wg.popItemErr:
if !q.isCtxRunCanceled() {
log.Error("Failed to pop item from queue %q (doRun): %v", q.GetName(), err)
}
return
- case <-q.ctxRun.Done():
- log.Debug("Queue %q is shutting down", q.GetName())
- return
}
}
}
diff --git a/modules/queue/workerqueue.go b/modules/queue/workerqueue.go
index b28fd88027..672e9a4114 100644
--- a/modules/queue/workerqueue.go
+++ b/modules/queue/workerqueue.go
@@ -32,8 +32,9 @@ type WorkerPoolQueue[T any] struct {
baseConfig *BaseConfig
baseQueue baseQueue
- batchChan chan []T
- flushChan chan flushType
+ batchChan chan []T
+ flushChan chan flushType
+ isFlushing atomic.Bool
batchLength int
workerNum int
@@ -42,7 +43,10 @@ type WorkerPoolQueue[T any] struct {
workerNumMu sync.Mutex
}
-type flushType chan struct{}
+type flushType struct {
+ timeout time.Duration
+ c chan struct{}
+}
var _ ManagedWorkerPoolQueue = (*WorkerPoolQueue[any])(nil)
@@ -104,12 +108,12 @@ func (q *WorkerPoolQueue[T]) FlushWithContext(ctx context.Context, timeout time.
if timeout > 0 {
after = time.After(timeout)
}
- c := make(flushType)
+ flush := flushType{timeout: timeout, c: make(chan struct{})}
// send flush request
// if it blocks, it means that there is a flush in progress or the queue hasn't been started yet
select {
- case q.flushChan <- c:
+ case q.flushChan <- flush:
case <-ctx.Done():
return ctx.Err()
case <-q.ctxRun.Done():
@@ -120,7 +124,7 @@ func (q *WorkerPoolQueue[T]) FlushWithContext(ctx context.Context, timeout time.
// wait for flush to finish
select {
- case <-c:
+ case <-flush.c:
return nil
case <-ctx.Done():
return ctx.Err()
diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go
index 6a2f4deaff..a9151482b4 100644
--- a/modules/repository/create_test.go
+++ b/modules/repository/create_test.go
@@ -38,8 +38,8 @@ func TestGetDirectorySize(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 1)
assert.NoError(t, err)
-
size, err := getDirectorySize(repo.RepoPath())
assert.NoError(t, err)
- assert.EqualValues(t, size, repo.Size)
+ repo.Size = 8165 // real size on the disk
+ assert.EqualValues(t, repo.Size, size)
}
diff --git a/modules/setting/attachment.go b/modules/setting/attachment.go
index 0fdabb5032..c11b0c478a 100644
--- a/modules/setting/attachment.go
+++ b/modules/setting/attachment.go
@@ -3,33 +3,33 @@
package setting
-// Attachment settings
-var Attachment = struct {
+type AttachmentSettingType struct {
Storage *Storage
AllowedTypes string
MaxSize int64
MaxFiles int
Enabled bool
-}{
- Storage: &Storage{},
- AllowedTypes: ".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip",
- MaxSize: 2048,
- MaxFiles: 5,
- Enabled: true,
}
+var Attachment AttachmentSettingType
+
func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
+ Attachment = AttachmentSettingType{
+ AllowedTypes: ".avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip",
+ MaxSize: 2048,
+ MaxFiles: 5,
+ Enabled: true,
+ }
sec, _ := rootCfg.GetSection("attachment")
if sec == nil {
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", nil)
return err
}
- Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
- Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(2048)
- Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
- Attachment.Enabled = sec.Key("ENABLED").MustBool(true)
-
+ Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(Attachment.AllowedTypes)
+ Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(Attachment.MaxSize)
+ Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(Attachment.MaxFiles)
+ Attachment.Enabled = sec.Key("ENABLED").MustBool(Attachment.Enabled)
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", sec)
return err
}
diff --git a/modules/setting/markup.go b/modules/setting/markup.go
index 6c2246342b..dfce8afa77 100644
--- a/modules/setting/markup.go
+++ b/modules/setting/markup.go
@@ -54,7 +54,7 @@ type MarkupRenderer struct {
type MarkupSanitizerRule struct {
Element string
AllowAttr string
- Regexp *regexp.Regexp
+ Regexp string
AllowDataURIImages bool
}
@@ -117,15 +117,24 @@ func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerR
regexpStr := sec.Key("REGEXP").Value()
if regexpStr != "" {
- // Validate when parsing the config that this is a valid regular
- // expression. Then we can use regexp.MustCompile(...) later.
- compiled, err := regexp.Compile(regexpStr)
+ hasPrefix := strings.HasPrefix(regexpStr, "^")
+ hasSuffix := strings.HasSuffix(regexpStr, "$")
+ if !hasPrefix || !hasSuffix {
+ log.Error("In markup.%s: REGEXP must start with ^ and end with $ to be strict", name)
+ // to avoid breaking existing user configurations and satisfy the strict requirement in addSanitizerRules
+ if !hasPrefix {
+ regexpStr = "^.*" + regexpStr
+ }
+ if !hasSuffix {
+ regexpStr += ".*$"
+ }
+ }
+ _, err := regexp.Compile(regexpStr)
if err != nil {
log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err)
return rule, false
}
-
- rule.Regexp = compiled
+ rule.Regexp = regexpStr
}
ok = true
diff --git a/modules/setting/ui.go b/modules/setting/ui.go
index a8dc11d097..db0fe9ef79 100644
--- a/modules/setting/ui.go
+++ b/modules/setting/ui.go
@@ -86,6 +86,7 @@ var UI = struct {
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
+ ExploreDefaultSort: "recentupdate",
PreferredTimestampTense: "mixed",
AmbiguousUnicodeDetection: true,
diff --git a/modules/svg/svg.go b/modules/svg/svg.go
index 8132978cac..fded9d0873 100644
--- a/modules/svg/svg.go
+++ b/modules/svg/svg.go
@@ -9,7 +9,7 @@ import (
"path"
"strings"
- gitea_html "code.gitea.io/gitea/modules/html"
+ gitea_html "code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public"
)
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 3ef11772dc..d5b32358da 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -10,12 +10,12 @@ import (
"html/template"
"net/url"
"reflect"
- "slices"
"strings"
"time"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
@@ -39,7 +39,7 @@ func NewFuncMap() template.FuncMap {
"Iif": iif,
"Eval": evalTokens,
"SafeHTML": safeHTML,
- "HTMLFormat": HTMLFormat,
+ "HTMLFormat": htmlutil.HTMLFormat,
"HTMLEscape": htmlEscape,
"QueryEscape": queryEscape,
"JSEscape": jsEscapeSafe,
@@ -184,23 +184,6 @@ func NewFuncMap() template.FuncMap {
}
}
-func HTMLFormat(s string, rawArgs ...any) template.HTML {
- args := slices.Clone(rawArgs)
- for i, v := range args {
- switch v := v.(type) {
- case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
- // for most basic types (including template.HTML which is safe), just do nothing and use it
- case string:
- args[i] = template.HTMLEscapeString(v)
- case fmt.Stringer:
- args[i] = template.HTMLEscapeString(v.String())
- default:
- args[i] = template.HTMLEscapeString(fmt.Sprint(v))
- }
- }
- return template.HTML(fmt.Sprintf(s, args...))
-}
-
// safeHTML render raw as HTML
func safeHTML(s any) template.HTML {
switch v := s.(type) {
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index b9fabb7016..3e17e86c66 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -61,10 +61,6 @@ func TestJSEscapeSafe(t *testing.T) {
assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, jsEscapeSafe(`&<>'"`))
}
-func TestHTMLFormat(t *testing.T) {
- assert.Equal(t, template.HTML("< < 1"), HTMLFormat("%s %s %d", "<", template.HTML("<"), 1))
-}
-
func TestSanitizeHTML(t *testing.T) {
assert.Equal(t, template.HTML(`link xss inline`), SanitizeHTML(`link xss inline`))
}
diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go
index afc1091516..f7dd408ee2 100644
--- a/modules/templates/util_avatar.go
+++ b/modules/templates/util_avatar.go
@@ -14,7 +14,7 @@ import (
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- gitea_html "code.gitea.io/gitea/modules/html"
+ gitea_html "code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/setting"
)
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index 1db1e4a111..5776eefced 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -16,6 +16,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/emoji"
+ "code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
@@ -62,19 +63,18 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me
}
msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
if len(msgLine) == 0 {
- return template.HTML("")
+ return ""
}
// we can safely assume that it will not return any error, since there
// shouldn't be any special HTML.
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
- Ctx: ut.ctx,
- DefaultLink: urlDefault,
- Metas: metas,
- }, template.HTMLEscapeString(msgLine))
+ Ctx: ut.ctx,
+ Metas: metas,
+ }, urlDefault, template.HTMLEscapeString(msgLine))
if err != nil {
log.Error("RenderCommitMessageSubject: %v", err)
- return template.HTML("")
+ return ""
}
return renderCodeBlock(template.HTML(renderedMessage))
}
@@ -94,9 +94,8 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem
}
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
- Ctx: ut.ctx,
- Metas: metas,
- ContentMode: markup.RenderContentAsComment,
+ Ctx: ut.ctx,
+ Metas: metas,
}, template.HTMLEscapeString(msgLine))
if err != nil {
log.Error("RenderCommitMessage: %v", err)
@@ -117,9 +116,8 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
// RenderIssueTitle renders issue/pull title with defined post processors
func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML {
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
- Ctx: ut.ctx,
- ContentMode: markup.RenderContentAsTitle,
- Metas: metas,
+ Ctx: ut.ctx,
+ Metas: metas,
}, template.HTMLEscapeString(text))
if err != nil {
log.Error("RenderIssueTitle: %v", err)
@@ -143,7 +141,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
if labelScope == "" {
// Regular label
- return HTMLFormat(`%s`,
+ return htmlutil.HTMLFormat(`%s`,
extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name))
}
@@ -177,7 +175,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
itemColor := "#" + hex.EncodeToString(itemBytes)
scopeColor := "#" + hex.EncodeToString(scopeBytes)
- return HTMLFormat(``+
+ return htmlutil.HTMLFormat(``+
`%s`+
`%s`+
``,
@@ -212,7 +210,7 @@ func reactionToEmoji(reaction string) template.HTML {
func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive
output, err := markdown.RenderString(&markup.RenderContext{
Ctx: ut.ctx,
- Metas: map[string]string{"mode": "document"},
+ Metas: markup.ComposeSimpleDocumentMetas(),
}, input)
if err != nil {
log.Error("RenderString: %v", err)
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index 2d331b8317..cf6d839cbf 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -47,10 +47,11 @@ mail@domain.com
}
var testMetas = map[string]string{
- "user": "user13",
- "repo": "repo11",
- "repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
- "mode": "comment",
+ "user": "user13",
+ "repo": "repo11",
+ "repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
+ "markdownLineBreakStyle": "comment",
+ "markupAllowShortIssuePattern": "true",
}
func TestMain(m *testing.M) {
@@ -75,8 +76,7 @@ func newTestRenderUtils() *RenderUtils {
func TestRenderCommitBody(t *testing.T) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
type args struct {
- msg string
- metas map[string]string
+ msg string
}
tests := []struct {
name string
@@ -108,39 +108,39 @@ func TestRenderCommitBody(t *testing.T) {
ut := newTestRenderUtils()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, tt.args.metas), "RenderCommitBody(%v, %v)", tt.args.msg, tt.args.metas)
+ assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, nil), "RenderCommitBody(%v, %v)", tt.args.msg, nil)
})
}
expected := `/just/a/path.bin
-https://example.com/file.bin
+https://example.com/file.bin
[local link](file.bin)
-[remote link](https://example.com)
+[remote link](https://example.com)
[[local link|file.bin]]
-[[remote link|https://example.com]]
+[[remote link|https://example.com]]
![local image](image.jpg)
-![remote image](https://example.com/image.jpg)
+![remote image](https://example.com/image.jpg)
[[local image|image.jpg]]
-[[remote link|https://example.com/image.jpg]]
+[[remote link|https://example.com/image.jpg]]
88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
-mail@domain.com
-@mention-user test
+mail@domain.com
+@mention-user test
#123
space`
assert.EqualValues(t, expected, string(newTestRenderUtils().RenderCommitBody(testInput(), testMetas)))
}
func TestRenderCommitMessage(t *testing.T) {
- expected := `space @mention-user `
+ expected := `space @mention-user `
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessage(testInput(), testMetas))
}
func TestRenderCommitMessageLinkSubject(t *testing.T) {
- expected := `space @mention-user`
+ expected := `space @mention-user`
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas))
}
@@ -164,11 +164,11 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@mention-user test
-#123
+#123
space
`
expected = strings.ReplaceAll(expected, "", " ")
- assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas)))
+ assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), nil)))
}
func TestRenderMarkdownToHtml(t *testing.T) {
diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go
index 9a54d63f20..2fbfce6b03 100644
--- a/modules/testlogger/testlogger.go
+++ b/modules/testlogger/testlogger.go
@@ -19,9 +19,10 @@ import (
)
var (
- prefix string
- SlowTest = 10 * time.Second
- SlowFlush = 5 * time.Second
+ prefix string
+ TestTimeout = 10 * time.Minute
+ TestSlowRun = 10 * time.Second
+ TestSlowFlush = 1 * time.Second
)
var WriterCloser = &testLoggerWriterCloser{}
@@ -89,79 +90,97 @@ func (w *testLoggerWriterCloser) Reset() {
w.Unlock()
}
+// Printf takes a format and args and prints the string to os.Stdout
+func Printf(format string, args ...any) {
+ if !log.CanColorStdout {
+ for i := 0; i < len(args); i++ {
+ if c, ok := args[i].(*log.ColoredValue); ok {
+ args[i] = c.Value()
+ }
+ }
+ }
+ _, _ = fmt.Fprintf(os.Stdout, format, args...)
+}
+
// PrintCurrentTest prints the current test to os.Stdout
func PrintCurrentTest(t testing.TB, skip ...int) func() {
t.Helper()
- start := time.Now()
+ runStart := time.Now()
actualSkip := util.OptionalArg(skip) + 1
_, filename, line, _ := runtime.Caller(actualSkip)
- if log.CanColorStdout {
- _, _ = fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", fmt.Formatter(log.NewColoredValue(t.Name())), strings.TrimPrefix(filename, prefix), line)
- } else {
- _, _ = fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", t.Name(), strings.TrimPrefix(filename, prefix), line)
- }
+ Printf("=== %s (%s:%d)\n", log.NewColoredValue(t.Name()), strings.TrimPrefix(filename, prefix), line)
+
WriterCloser.pushT(t)
- return func() {
- took := time.Since(start)
- if took > SlowTest {
- if log.CanColorStdout {
- _, _ = fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgYellow)), fmt.Formatter(log.NewColoredValue(took, log.Bold, log.FgYellow)))
- } else {
- _, _ = fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", t.Name(), took)
+ timeoutChecker := time.AfterFunc(TestTimeout, func() {
+ l := 128 * 1024
+ var stack []byte
+ for {
+ stack = make([]byte, l)
+ n := runtime.Stack(stack, true)
+ if n <= l {
+ stack = stack[:n]
+ break
}
+ l = n
}
- timer := time.AfterFunc(SlowFlush, func() {
- if log.CanColorStdout {
- _, _ = fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), SlowFlush)
- } else {
- _, _ = fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", t.Name(), SlowFlush)
- }
+ Printf("!!! %s ... timeout: %v ... stacktrace:\n%s\n\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestTimeout, string(stack))
+ })
+ return func() {
+ flushStart := time.Now()
+ slowFlushChecker := time.AfterFunc(TestSlowFlush, func() {
+ Printf("+++ %s ... still flushing after %v ...\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestSlowFlush)
})
- if err := queue.GetManager().FlushAll(context.Background(), time.Minute); err != nil {
+ if err := queue.GetManager().FlushAll(context.Background(), -1); err != nil {
t.Errorf("Flushing queues failed with error %v", err)
}
- timer.Stop()
- flushTook := time.Since(start) - took
- if flushTook > SlowFlush {
- if log.CanColorStdout {
- _, _ = fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), fmt.Formatter(log.NewColoredValue(flushTook, log.Bold, log.FgRed)))
- } else {
- _, _ = fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", t.Name(), flushTook)
- }
+ slowFlushChecker.Stop()
+ timeoutChecker.Stop()
+
+ runDuration := time.Since(runStart)
+ flushDuration := time.Since(flushStart)
+ if runDuration > TestSlowRun {
+ Printf("+++ %s is a slow test (run: %v, flush: %v)\n", log.NewColoredValue(t.Name(), log.Bold, log.FgYellow), runDuration, flushDuration)
}
WriterCloser.popT()
}
}
-// Printf takes a format and args and prints the string to os.Stdout
-func Printf(format string, args ...any) {
- if log.CanColorStdout {
- for i := 0; i < len(args); i++ {
- args[i] = log.NewColoredValue(args[i])
- }
- }
- _, _ = fmt.Fprintf(os.Stdout, "\t"+format, args...)
-}
-
// TestLogEventWriter is a logger which will write to the testing log
type TestLogEventWriter struct {
*log.EventWriterBaseImpl
}
-// NewTestLoggerWriter creates a TestLogEventWriter as a log.LoggerProvider
-func NewTestLoggerWriter(name string, mode log.WriterMode) log.EventWriter {
+// newTestLoggerWriter creates a TestLogEventWriter as a log.LoggerProvider
+func newTestLoggerWriter(name string, mode log.WriterMode) log.EventWriter {
w := &TestLogEventWriter{}
w.EventWriterBaseImpl = log.NewEventWriterBase(name, "test-log-writer", mode)
w.OutputWriteCloser = WriterCloser
return w
}
-func init() {
+func Init() {
const relFilePath = "modules/testlogger/testlogger.go"
_, filename, _, _ := runtime.Caller(0)
if !strings.HasSuffix(filename, relFilePath) {
panic("source code file path doesn't match expected: " + relFilePath)
}
prefix = strings.TrimSuffix(filename, relFilePath)
+
+ log.RegisterEventWriter("test", newTestLoggerWriter)
+
+ duration, err := time.ParseDuration(os.Getenv("GITEA_TEST_SLOW_RUN"))
+ if err == nil && duration > 0 {
+ TestSlowRun = duration
+ }
+
+ duration, err = time.ParseDuration(os.Getenv("GITEA_TEST_SLOW_FLUSH"))
+ if err == nil && duration > 0 {
+ TestSlowFlush = duration
+ }
+}
+
+func Fatalf(format string, args ...any) {
+ Printf(format+"\n", args...)
+ os.Exit(1)
}
diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go
index 6aec5c285e..8cb3d278ce 100644
--- a/modules/typesniffer/typesniffer.go
+++ b/modules/typesniffer/typesniffer.go
@@ -5,10 +5,12 @@ package typesniffer
import (
"bytes"
+ "encoding/binary"
"fmt"
"io"
"net/http"
"regexp"
+ "slices"
"strings"
"code.gitea.io/gitea/modules/util"
@@ -18,10 +20,10 @@ import (
const sniffLen = 1024
const (
- // SvgMimeType MIME type of SVG images.
- SvgMimeType = "image/svg+xml"
- // ApplicationOctetStream MIME type of binary files.
- ApplicationOctetStream = "application/octet-stream"
+ MimeTypeImageSvg = "image/svg+xml"
+ MimeTypeImageAvif = "image/avif"
+
+ MimeTypeApplicationOctetStream = "application/octet-stream"
)
var (
@@ -47,7 +49,7 @@ func (ct SniffedType) IsImage() bool {
// IsSvgImage detects if data is an SVG image format
func (ct SniffedType) IsSvgImage() bool {
- return strings.Contains(ct.contentType, SvgMimeType)
+ return strings.Contains(ct.contentType, MimeTypeImageSvg)
}
// IsPDF detects if data is a PDF format
@@ -81,6 +83,26 @@ func (ct SniffedType) GetMimeType() string {
return strings.SplitN(ct.contentType, ";", 2)[0]
}
+// https://en.wikipedia.org/wiki/ISO_base_media_file_format#File_type_box
+func detectFileTypeBox(data []byte) (brands []string, found bool) {
+ if len(data) < 12 {
+ return nil, false
+ }
+ boxSize := int(binary.BigEndian.Uint32(data[:4]))
+ if boxSize < 12 || boxSize > len(data) {
+ return nil, false
+ }
+ tag := string(data[4:8])
+ if tag != "ftyp" {
+ return nil, false
+ }
+ brands = append(brands, string(data[8:12]))
+ for i := 16; i+4 <= boxSize; i += 4 {
+ brands = append(brands, string(data[i:i+4]))
+ }
+ return brands, true
+}
+
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty.
func DetectContentType(data []byte) SniffedType {
if len(data) == 0 {
@@ -94,7 +116,6 @@ func DetectContentType(data []byte) SniffedType {
}
// SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888
-
detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")
detectByXML := strings.Contains(ct, "text/xml")
if detectByHTML || detectByXML {
@@ -102,7 +123,7 @@ func DetectContentType(data []byte) SniffedType {
dataProcessed = bytes.TrimSpace(dataProcessed)
if detectByHTML && svgTagRegex.Match(dataProcessed) ||
detectByXML && svgTagInXMLRegex.Match(dataProcessed) {
- ct = SvgMimeType
+ ct = MimeTypeImageSvg
}
}
@@ -116,6 +137,11 @@ func DetectContentType(data []byte) SniffedType {
}
}
+ fileTypeBrands, found := detectFileTypeBox(data)
+ if found && slices.Contains(fileTypeBrands, "avif") {
+ ct = MimeTypeImageAvif
+ }
+
if ct == "application/ogg" {
dataHead := data
if len(dataHead) > 256 {
diff --git a/modules/typesniffer/typesniffer_test.go b/modules/typesniffer/typesniffer_test.go
index 731fac11e7..3e5db3308b 100644
--- a/modules/typesniffer/typesniffer_test.go
+++ b/modules/typesniffer/typesniffer_test.go
@@ -134,3 +134,33 @@ func TestDetectContentTypeOgg(t *testing.T) {
assert.NoError(t, err)
assert.True(t, st.IsVideo())
}
+
+func TestDetectFileTypeBox(t *testing.T) {
+ _, found := detectFileTypeBox([]byte("\x00\x00\xff\xffftypAAAA...."))
+ assert.False(t, found)
+
+ brands, found := detectFileTypeBox([]byte("\x00\x00\x00\x0cftypAAAA"))
+ assert.True(t, found)
+ assert.Equal(t, []string{"AAAA"}, brands)
+
+ brands, found = detectFileTypeBox([]byte("\x00\x00\x00\x10ftypAAAA....BBBB"))
+ assert.True(t, found)
+ assert.Equal(t, []string{"AAAA"}, brands)
+
+ brands, found = detectFileTypeBox([]byte("\x00\x00\x00\x14ftypAAAA....BBBB"))
+ assert.True(t, found)
+ assert.Equal(t, []string{"AAAA", "BBBB"}, brands)
+
+ _, found = detectFileTypeBox([]byte("\x00\x00\x00\x14ftypAAAA....BBB"))
+ assert.False(t, found)
+
+ brands, found = detectFileTypeBox([]byte("\x00\x00\x00\x13ftypAAAA....BBB"))
+ assert.True(t, found)
+ assert.Equal(t, []string{"AAAA"}, brands)
+}
+
+func TestDetectContentTypeAvif(t *testing.T) {
+ buf := []byte("\x00\x00\x00\x20ftypavif.......................")
+ st := DetectContentType(buf)
+ assert.Equal(t, MimeTypeImageAvif, st.contentType)
+}
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index a1e3c9804b..14a1a8d1c4 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -55,11 +55,20 @@ func ListForks(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx))
+ forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetForks", err)
+ ctx.Error(http.StatusInternalServerError, "FindForks", err)
return
}
+ if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadOwners", err)
+ return
+ }
+ if err := repo_model.RepositoryList(forks).LoadUnits(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadUnits", err)
+ return
+ }
+
apiForks := make([]*api.Repository, len(forks))
for i, fork := range forks {
permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer)
@@ -70,7 +79,7 @@ func ListForks(ctx *context.APIContext) {
apiForks[i] = convert.ToRepo(ctx, fork, permission)
}
- ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks))
+ ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, apiForks)
}
diff --git a/routers/common/markup.go b/routers/common/markup.go
index c8cc1a5ff1..dd6b286109 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -47,11 +47,12 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa
switch mode {
case "gfm": // legacy mode, do nothing
case "comment":
- renderCtx.ContentMode = markup.RenderContentAsComment
+ renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "comment"}
case "wiki":
- renderCtx.ContentMode = markup.RenderContentAsWiki
+ renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "document", "markupContentMode": "wiki"}
case "file":
// render the repo file content by its extension
+ renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "document"}
renderCtx.MarkupType = ""
renderCtx.RelativePath = filePath
renderCtx.InStandalonePage = true
@@ -74,10 +75,12 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa
if repo != nil && repo.Repository != nil {
renderCtx.Repo = repo.Repository
- if renderCtx.ContentMode == markup.RenderContentAsComment {
- renderCtx.Metas = repo.Repository.ComposeMetas(ctx)
- } else {
+ if mode == "file" {
renderCtx.Metas = repo.Repository.ComposeDocumentMetas(ctx)
+ } else if mode == "wiki" {
+ renderCtx.Metas = repo.Repository.ComposeWikiMetas(ctx)
+ } else if mode == "comment" {
+ renderCtx.Metas = repo.Repository.ComposeMetas(ctx)
}
}
if err := markup.Render(renderCtx, strings.NewReader(text), ctx.Resp); err != nil {
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 730d68051b..75f94de0ed 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -122,6 +122,8 @@ func SignInOAuthCallback(ctx *context.Context) {
}
if err, ok := err.(*go_oauth2.RetrieveError); ok {
ctx.Flash.Error("OAuth2 RetrieveError: "+err.Error(), true)
+ ctx.Redirect(setting.AppSubURL + "/user/login")
+ return
}
ctx.ServerError("UserSignIn", err)
return
diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go
index faea34959f..d844d42421 100644
--- a/routers/web/auth/oauth2_provider.go
+++ b/routers/web/auth/oauth2_provider.go
@@ -80,12 +80,12 @@ func (err errCallback) Error() string {
}
type userInfoResponse struct {
- Sub string `json:"sub"`
- Name string `json:"name"`
- Username string `json:"preferred_username"`
- Email string `json:"email"`
- Picture string `json:"picture"`
- Groups []string `json:"groups"`
+ Sub string `json:"sub"`
+ Name string `json:"name"`
+ PreferredUsername string `json:"preferred_username"`
+ Email string `json:"email"`
+ Picture string `json:"picture"`
+ Groups []string `json:"groups"`
}
// InfoOAuth manages request for userinfo endpoint
@@ -97,11 +97,11 @@ func InfoOAuth(ctx *context.Context) {
}
response := &userInfoResponse{
- Sub: fmt.Sprint(ctx.Doer.ID),
- Name: ctx.Doer.FullName,
- Username: ctx.Doer.Name,
- Email: ctx.Doer.Email,
- Picture: ctx.Doer.AvatarLink(ctx),
+ Sub: fmt.Sprint(ctx.Doer.ID),
+ Name: ctx.Doer.FullName,
+ PreferredUsername: ctx.Doer.Name,
+ Email: ctx.Doer.Email,
+ Picture: ctx.Doer.AvatarLink(ctx),
}
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer)
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index a273515c8a..afc2c343a6 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -56,7 +56,7 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content
Links: markup.Links{
Base: act.GetRepoLink(ctx),
},
- Metas: map[string]string{
+ Metas: map[string]string{ // FIXME: not right here, it should use issue to compose the metas
"user": act.GetRepoUserName(ctx),
"repo": act.GetRepoName(ctx),
},
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index 08cbcd9e12..6dd2d14cc6 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -46,9 +46,7 @@ func showUserFeed(ctx *context.Context, formatType string) {
Links: markup.Links{
Base: ctx.ContextUser.HTMLURL(),
},
- Metas: map[string]string{
- "user": ctx.ContextUser.GetDisplayName(),
- },
+ Metas: markup.ComposeSimpleDocumentMetas(),
}, ctx.ContextUser.Description)
if err != nil {
ctx.ServerError("RenderString", err)
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 544f5362b8..18648d33cd 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -189,7 +189,7 @@ func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool {
Base: profileDbRepo.Link(),
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
},
- Metas: map[string]string{"mode": "document"},
+ Metas: markup.ComposeSimpleDocumentMetas(),
}, bytes); err != nil {
log.Error("failed to RenderString: %v", err)
} else {
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 485bd927fa..e30129bb44 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"net/http"
- "strconv"
"strings"
"time"
@@ -290,8 +289,8 @@ func SettingsPost(ctx *context.Context) {
return
}
- m, err := selectPushMirrorByForm(ctx, form, repo)
- if err != nil {
+ m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+ if m == nil {
ctx.NotFound("", nil)
return
}
@@ -317,15 +316,13 @@ func SettingsPost(ctx *context.Context) {
return
}
- id, err := strconv.ParseInt(form.PushMirrorID, 10, 64)
- if err != nil {
- ctx.ServerError("UpdatePushMirrorIntervalPushMirrorID", err)
+ m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+ if m == nil {
+ ctx.NotFound("", nil)
return
}
- m := &repo_model.PushMirror{
- ID: id,
- Interval: interval,
- }
+
+ m.Interval = interval
if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil {
ctx.ServerError("UpdatePushMirrorInterval", err)
return
@@ -334,7 +331,10 @@ func SettingsPost(ctx *context.Context) {
// If we observed its implementation in the context of `push-mirror-sync` where it
// is evident that pushing to the queue is necessary for updates.
// So, there are updates within the given interval, it is necessary to update the queue accordingly.
- mirror_service.AddPushMirrorToQueue(m.ID)
+ if !ctx.FormBool("push_mirror_defer_sync") {
+ // push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately
+ mirror_service.AddPushMirrorToQueue(m.ID)
+ }
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
@@ -348,18 +348,18 @@ func SettingsPost(ctx *context.Context) {
// as an error on the UI for this action
ctx.Data["Err_RepoName"] = nil
- m, err := selectPushMirrorByForm(ctx, form, repo)
- if err != nil {
+ m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+ if m == nil {
ctx.NotFound("", nil)
return
}
- if err = mirror_service.RemovePushMirrorRemote(ctx, m); err != nil {
+ if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil {
ctx.ServerError("RemovePushMirrorRemote", err)
return
}
- if err = repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
+ if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
ctx.ServerError("DeletePushMirrorByID", err)
return
}
@@ -995,24 +995,3 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R
}
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
}
-
-func selectPushMirrorByForm(ctx *context.Context, form *forms.RepoSettingForm, repo *repo_model.Repository) (*repo_model.PushMirror, error) {
- id, err := strconv.ParseInt(form.PushMirrorID, 10, 64)
- if err != nil {
- return nil, err
- }
-
- pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{})
- if err != nil {
- return nil, err
- }
-
- for _, m := range pushMirrors {
- if m.ID == id {
- m.Repo = repo
- return m, nil
- }
- }
-
- return nil, fmt.Errorf("PushMirror[%v] not associated to repository %v", id, repo)
-}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 7030f6d8a9..5d68ace29b 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -1151,26 +1151,25 @@ func Forks(ctx *context.Context) {
if page <= 0 {
page = 1
}
+ pageSize := setting.ItemsPerPage
- pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.ItemsPerPage, page, 5)
- ctx.Data["Page"] = pager
-
- forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{
- Page: pager.Paginater.Current(),
- PageSize: setting.ItemsPerPage,
+ forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, db.ListOptions{
+ Page: page,
+ PageSize: pageSize,
})
if err != nil {
- ctx.ServerError("GetForks", err)
+ ctx.ServerError("FindForks", err)
return
}
- for _, fork := range forks {
- if err = fork.LoadOwner(ctx); err != nil {
- ctx.ServerError("LoadOwner", err)
- return
- }
+ if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
+ ctx.ServerError("LoadAttributes", err)
+ return
}
+ pager := context.NewPagination(int(total), pageSize, page, 5)
+ ctx.Data["Page"] = pager
+
ctx.Data["Forks"] = forks
ctx.HTML(http.StatusOK, tplForks)
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 2b8312f10a..2732a67e71 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -289,9 +289,8 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
}
rctx := &markup.RenderContext{
- Ctx: ctx,
- ContentMode: markup.RenderContentAsWiki,
- Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
+ Ctx: ctx,
+ Metas: ctx.Repo.Repository.ComposeWikiMetas(ctx),
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
@@ -327,7 +326,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
if rctx.SidebarTocNode != nil {
sb := &strings.Builder{}
- err = markdown.SpecializedMarkdown().Renderer().Render(sb, nil, rctx.SidebarTocNode)
+ err = markdown.SpecializedMarkdown(rctx).Renderer().Render(sb, nil, rctx.SidebarTocNode)
if err != nil {
log.Error("Failed to render wiki sidebar TOC: %v", err)
} else {
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index ef111cff80..9467b0986b 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -50,7 +50,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
ctx.Data["OpenIDs"] = openIDs
if len(ctx.ContextUser.Description) != 0 {
content, err := markdown.RenderString(&markup.RenderContext{
- Metas: map[string]string{"mode": "document"},
+ Metas: markup.ComposeSimpleDocumentMetas(),
Ctx: ctx,
}, ctx.ContextUser.Description)
if err != nil {
diff --git a/services/auth/basic.go b/services/auth/basic.go
index 90bd642370..1f6c3a442d 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -5,6 +5,7 @@
package auth
import (
+ "errors"
"net/http"
"strings"
@@ -141,6 +142,15 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
}
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
+ // Check if the user has webAuthn registration
+ hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
+ if err != nil {
+ return nil, err
+ }
+ if hasWebAuthn {
+ return nil, errors.New("Basic authorization is not allowed while webAuthn enrolled")
+ }
+
if err := validateTOTP(req, u); err != nil {
return nil, err
}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index d27bbca894..8e663084f8 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -122,7 +122,7 @@ type RepoSettingForm struct {
MirrorPassword string
LFS bool `form:"mirror_lfs"`
LFSEndpoint string `form:"mirror_lfs_endpoint"`
- PushMirrorID string
+ PushMirrorID int64
PushMirrorAddress string
PushMirrorUsername string
PushMirrorPassword string
diff --git a/services/gitdiff/testdata/academic-module/description b/services/gitdiff/testdata/academic-module/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/services/gitdiff/testdata/academic-module/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/services/gitdiff/testdata/academic-module/info/exclude b/services/gitdiff/testdata/academic-module/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/services/gitdiff/testdata/academic-module/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go
index 44218d6fb3..e029bbb1d6 100644
--- a/services/mirror/mirror.go
+++ b/services/mirror/mirror.go
@@ -8,7 +8,6 @@ import (
"fmt"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
@@ -119,14 +118,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
return nil
}
-func queueHandler(items ...*SyncRequest) []*SyncRequest {
- for _, req := range items {
- doMirrorSync(graceful.GetManager().ShutdownContext(), req)
- }
- return nil
-}
-
// InitSyncMirrors initializes a go routine to sync the mirrors
func InitSyncMirrors() {
- StartSyncMirrors(queueHandler)
+ StartSyncMirrors()
}
diff --git a/services/mirror/queue.go b/services/mirror/queue.go
index 0d9a624730..ca5e2c7272 100644
--- a/services/mirror/queue.go
+++ b/services/mirror/queue.go
@@ -28,12 +28,19 @@ type SyncRequest struct {
ReferenceID int64 // RepoID for pull mirror, MirrorID for push mirror
}
+func queueHandler(items ...*SyncRequest) []*SyncRequest {
+ for _, req := range items {
+ doMirrorSync(graceful.GetManager().ShutdownContext(), req)
+ }
+ return nil
+}
+
// StartSyncMirrors starts a go routine to sync the mirrors
-func StartSyncMirrors(queueHandle func(data ...*SyncRequest) []*SyncRequest) {
+func StartSyncMirrors() {
if !setting.Mirror.Enabled {
return
}
- mirrorQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "mirror", queueHandle)
+ mirrorQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "mirror", queueHandler)
if mirrorQueue == nil {
log.Fatal("Unable to create mirror queue")
}
diff --git a/services/repository/adopt_test.go b/services/repository/adopt_test.go
index 38949c7602..123cedc1f2 100644
--- a/services/repository/adopt_test.go
+++ b/services/repository/adopt_test.go
@@ -89,7 +89,7 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
func TestAdoptRepository(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- assert.NoError(t, unittest.CopyDir(filepath.Join(setting.RepoRootPath, "user2", "repo1.git"), filepath.Join(setting.RepoRootPath, "user2", "test-adopt.git")))
+ assert.NoError(t, unittest.SyncDirs(filepath.Join(setting.RepoRootPath, "user2", "repo1.git"), filepath.Join(setting.RepoRootPath, "user2", "test-adopt.git")))
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
_, err := AdoptRepository(db.DefaultContext, user2, user2, CreateRepoOptions{Name: "test-adopt"})
assert.NoError(t, err)
diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go
index 01c58f0ce4..c33369d047 100644
--- a/services/repository/archiver/archiver.go
+++ b/services/repository/archiver/archiver.go
@@ -68,7 +68,7 @@ func (e RepoRefNotFoundError) Is(err error) bool {
}
// NewRequest creates an archival request, based on the URI. The
-// resulting ArchiveRequest is suitable for being passed to ArchiveRepository()
+// resulting ArchiveRequest is suitable for being passed to Await()
// if it's determined that the request still needs to be satisfied.
func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) {
r := &ArchiveRequest{
@@ -151,13 +151,14 @@ func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver
}
}
+// doArchive satisfies the ArchiveRequest being passed in. Processing
+// will occur in a separate goroutine, as this phase may take a while to
+// complete. If the archive already exists, doArchive will not do
+// anything. In all cases, the caller should be examining the *ArchiveRequest
+// being returned for completion, as it may be different than the one they passed
+// in.
func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
- txCtx, committer, err := db.TxContext(ctx)
- if err != nil {
- return nil, err
- }
- defer committer.Close()
- ctx, _, finished := process.GetManager().AddContext(txCtx, fmt.Sprintf("ArchiveRequest[%d]: %s", r.RepoID, r.GetArchiveName()))
+ ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("ArchiveRequest[%d]: %s", r.RepoID, r.GetArchiveName()))
defer finished()
archiver, err := repo_model.GetRepoArchiver(ctx, r.RepoID, r.Type, r.CommitID)
@@ -192,7 +193,7 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver
return nil, err
}
}
- return archiver, committer.Commit()
+ return archiver, nil
}
if !errors.Is(err, os.ErrNotExist) {
@@ -261,17 +262,7 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver
}
}
- return archiver, committer.Commit()
-}
-
-// ArchiveRepository satisfies the ArchiveRequest being passed in. Processing
-// will occur in a separate goroutine, as this phase may take a while to
-// complete. If the archive already exists, ArchiveRepository will not do
-// anything. In all cases, the caller should be examining the *ArchiveRequest
-// being returned for completion, as it may be different than the one they passed
-// in.
-func ArchiveRepository(ctx context.Context, request *ArchiveRequest) (*repo_model.RepoArchiver, error) {
- return doArchive(ctx, request)
+ return archiver, nil
}
var archiverQueue *queue.WorkerPoolQueue[*ArchiveRequest]
@@ -281,8 +272,10 @@ func Init(ctx context.Context) error {
handler := func(items ...*ArchiveRequest) []*ArchiveRequest {
for _, archiveReq := range items {
log.Trace("ArchiverData Process: %#v", archiveReq)
- if _, err := doArchive(ctx, archiveReq); err != nil {
+ if archiver, err := doArchive(ctx, archiveReq); err != nil {
log.Error("Archive %v failed: %v", archiveReq, err)
+ } else {
+ log.Trace("ArchiverData Success: %#v", archiver)
}
}
return nil
diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go
index ec6e9dfac3..b3f3ed7bf3 100644
--- a/services/repository/archiver/archiver_test.go
+++ b/services/repository/archiver/archiver_test.go
@@ -80,13 +80,13 @@ func TestArchive_Basic(t *testing.T) {
inFlight[1] = tgzReq
inFlight[2] = secondReq
- ArchiveRepository(db.DefaultContext, zipReq)
- ArchiveRepository(db.DefaultContext, tgzReq)
- ArchiveRepository(db.DefaultContext, secondReq)
+ doArchive(db.DefaultContext, zipReq)
+ doArchive(db.DefaultContext, tgzReq)
+ doArchive(db.DefaultContext, secondReq)
// Make sure sending an unprocessed request through doesn't affect the queue
// count.
- ArchiveRepository(db.DefaultContext, zipReq)
+ doArchive(db.DefaultContext, zipReq)
// Sleep two seconds to make sure the queue doesn't change.
time.Sleep(2 * time.Second)
@@ -101,7 +101,7 @@ func TestArchive_Basic(t *testing.T) {
// We still have the other three stalled at completion, waiting to remove
// from archiveInProgress. Try to submit this new one before its
// predecessor has cleared out of the queue.
- ArchiveRepository(db.DefaultContext, zipReq2)
+ doArchive(db.DefaultContext, zipReq2)
// Now we'll submit a request and TimedWaitForCompletion twice, before and
// after we release it. We should trigger both the timeout and non-timeout
@@ -109,7 +109,7 @@ func TestArchive_Basic(t *testing.T) {
timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz")
assert.NoError(t, err)
assert.NotNil(t, timedReq)
- ArchiveRepository(db.DefaultContext, timedReq)
+ doArchive(db.DefaultContext, timedReq)
zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
assert.NoError(t, err)
diff --git a/services/repository/fork.go b/services/repository/fork.go
index 5b24015a03..bc4fdf8562 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
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/gitrepo"
@@ -20,6 +21,8 @@ import (
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
+
+ "xorm.io/builder"
)
// ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error.
@@ -247,3 +250,24 @@ func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Reposit
return err
}
+
+type findForksOptions struct {
+ db.ListOptions
+ RepoID int64
+ Doer *user_model.User
+}
+
+func (opts findForksOptions) ToConds() builder.Cond {
+ return builder.Eq{"fork_id": opts.RepoID}.And(
+ repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid),
+ )
+}
+
+// FindForks returns all the forks of the repository
+func FindForks(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, listOptions db.ListOptions) ([]*repo_model.Repository, int64, error) {
+ return db.FindAndCount[repo_model.Repository](ctx, findForksOptions{
+ ListOptions: listOptions,
+ RepoID: repo.ID,
+ Doer: doer,
+ })
+}
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index d0805c85bc..d5e09939c5 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -52,7 +52,7 @@
{{.ID}}
- {{.Name}}
+ {{if and DefaultShowFullName .FullName}}{{.FullName}} ({{.Name}}){{else}}{{.Name}}{{end}}
{{if .Visibility.IsPrivate}}
{{svg "octicon-lock"}}
{{end}}
diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl
index 412c59b60e..725b67c651 100644
--- a/templates/repo/forks.tmpl
+++ b/templates/repo/forks.tmpl
@@ -5,12 +5,14 @@
{{ctx.Locale.Tr "repo.forks"}}
+
{{range .Forks}}
-
- {{ctx.AvatarUtils.Avatar .Owner}}
- {{.Owner.Name}} / {{.Name}}
+
+ {{ctx.AvatarUtils.Avatar .Owner}}
+ {{.Owner.Name}} / {{.Name}}
{{end}}
+
{{template "base/paginate" .}}
diff --git a/templates/repo/issue/fields/markdown.tmpl b/templates/repo/issue/fields/markdown.tmpl
index f6995328dd..da8f5e6bdf 100644
--- a/templates/repo/issue/fields/markdown.tmpl
+++ b/templates/repo/issue/fields/markdown.tmpl
@@ -1,3 +1,3 @@
- {{ctx.RenderUtils.MarkdownToHtml .item.Attributes.value}}
+ {{ctx.RenderUtils.MarkdownToHtml .item.Attributes.value}}
diff --git a/templates/repo/projects/new.tmpl b/templates/repo/projects/new.tmpl
index e70f3bca87..67abfe24be 100644
--- a/templates/repo/projects/new.tmpl
+++ b/templates/repo/projects/new.tmpl
@@ -1,5 +1,5 @@
{{template "base/head" .}}
-
+
{{template "repo/header" .}}
{{template "projects/new" .}}
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index fb5bc14fe2..89bb371e7c 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -29,7 +29,7 @@
{{if .ReadmeInList}}
{{svg "octicon-book" 16 "tw-mr-2"}}
- {{.FileName}}
+ {{.FileName}}
{{else}}
{{template "repo/file_info" .}}
{{end}}
diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl
index be710675d5..a2764ba608 100644
--- a/templates/user/dashboard/repolist.tmpl
+++ b/templates/user/dashboard/repolist.tmpl
@@ -45,7 +45,7 @@ data.teamId = {{.Team.ID}};
{{end}}
{{if not .ContextUser.IsOrganization}}
-data.organizations = [{{range .Orgs}}{'name': {{.Name}}, 'num_repos': {{.NumRepos}}, 'org_visibility': {{.Visibility}}},{{end}}];
+data.organizations = [{{range .Orgs}}{'name': {{.Name}}, 'full_name': {{.FullName}}, 'num_repos': {{.NumRepos}}, 'org_visibility': {{.Visibility}}},{{end}}];
data.isOrganization = false;
data.organizationsTotalCount = {{.UserOrgsCount}};
data.canCreateOrganization = {{.SignedUser.CanCreateOrganization}};
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/description b/tests/gitea-repositories-meta/migration/lfs-test.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/migration/lfs-test.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/description b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/info/exclude b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/description b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/info/exclude b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/description b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/info/exclude b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/org3/repo3.git/description b/tests/gitea-repositories-meta/org3/repo3.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/org3/repo3.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org3/repo3.git/info/exclude b/tests/gitea-repositories-meta/org3/repo3.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/org3/repo3.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/org3/repo5.git/description b/tests/gitea-repositories-meta/org3/repo5.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/org3/repo5.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org3/repo5.git/info/exclude b/tests/gitea-repositories-meta/org3/repo5.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/org3/repo5.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/description b/tests/gitea-repositories-meta/org41/repo61.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/org41/repo61.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/info/exclude b/tests/gitea-repositories-meta/org41/repo61.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/org41/repo61.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/objects/.keep b/tests/gitea-repositories-meta/org41/repo61.git/objects/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/gitea-repositories-meta/org41/repo61.git/refs/.keep b/tests/gitea-repositories-meta/org41/repo61.git/refs/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/info/exclude b/tests/gitea-repositories-meta/org42/search-by-path.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/org42/search-by-path.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/refs/.keep b/tests/gitea-repositories-meta/org42/search-by-path.git/refs/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/description b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/info/exclude b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/description b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/info/exclude b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/description b/tests/gitea-repositories-meta/user12/repo10.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user12/repo10.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/info/exclude b/tests/gitea-repositories-meta/user12/repo10.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user12/repo10.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/description b/tests/gitea-repositories-meta/user13/repo11.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user13/repo11.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/info/exclude b/tests/gitea-repositories-meta/user13/repo11.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user13/repo11.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/description b/tests/gitea-repositories-meta/user2/commits_search_test.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/commits_search_test.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/info/exclude b/tests/gitea-repositories-meta/user2/commits_search_test.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/commits_search_test.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/description b/tests/gitea-repositories-meta/user2/commitsonpr.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/commitsonpr.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/info/exclude b/tests/gitea-repositories-meta/user2/commitsonpr.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/commitsonpr.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/description b/tests/gitea-repositories-meta/user2/git_hooks_test.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/git_hooks_test.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/info/exclude b/tests/gitea-repositories-meta/user2/git_hooks_test.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/git_hooks_test.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/glob.git/description b/tests/gitea-repositories-meta/user2/glob.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/glob.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/glob.git/info/exclude b/tests/gitea-repositories-meta/user2/glob.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/glob.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/readme-test.git/info/exclude b/tests/gitea-repositories-meta/user2/readme-test.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/readme-test.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/repo-release.git/description b/tests/gitea-repositories-meta/user2/repo-release.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/repo-release.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo-release.git/info/exclude b/tests/gitea-repositories-meta/user2/repo-release.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/repo-release.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/description b/tests/gitea-repositories-meta/user2/repo1.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/repo1.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/info/exclude b/tests/gitea-repositories-meta/user2/repo1.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/repo1.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/description b/tests/gitea-repositories-meta/user2/repo1.wiki.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/repo1.wiki.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/info/exclude b/tests/gitea-repositories-meta/user2/repo1.wiki.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/repo1.wiki.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/description b/tests/gitea-repositories-meta/user2/repo15.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/repo15.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/info/exclude b/tests/gitea-repositories-meta/user2/repo15.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/repo15.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/description b/tests/gitea-repositories-meta/user2/repo16.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/repo16.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/info/exclude b/tests/gitea-repositories-meta/user2/repo16.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/repo16.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/description b/tests/gitea-repositories-meta/user2/repo2.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/repo2.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/info/exclude b/tests/gitea-repositories-meta/user2/repo2.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/repo2.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/description b/tests/gitea-repositories-meta/user2/repo20.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/repo20.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/info/exclude b/tests/gitea-repositories-meta/user2/repo20.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/repo20.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/description b/tests/gitea-repositories-meta/user2/test_commit_revert.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/test_commit_revert.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/info/exclude b/tests/gitea-repositories-meta/user2/test_commit_revert.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/test_commit_revert.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/description b/tests/gitea-repositories-meta/user2/utf8.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user2/utf8.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/info/exclude b/tests/gitea-repositories-meta/user2/utf8.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user2/utf8.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/description b/tests/gitea-repositories-meta/user27/repo49.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user27/repo49.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/info/exclude b/tests/gitea-repositories-meta/user27/repo49.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user27/repo49.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user27/template1.git/description b/tests/gitea-repositories-meta/user27/template1.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user27/template1.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user27/template1.git/info/exclude b/tests/gitea-repositories-meta/user27/template1.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user27/template1.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user30/empty.git/objects/.keep b/tests/gitea-repositories-meta/user30/empty.git/objects/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/gitea-repositories-meta/user30/empty.git/refs/.keep b/tests/gitea-repositories-meta/user30/empty.git/refs/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/gitea-repositories-meta/user30/renderer.git/info/exclude b/tests/gitea-repositories-meta/user30/renderer.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user30/renderer.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user30/renderer.git/refs/.keep b/tests/gitea-repositories-meta/user30/renderer.git/refs/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/description b/tests/gitea-repositories-meta/user40/repo60.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user40/repo60.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/info/exclude b/tests/gitea-repositories-meta/user40/repo60.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user40/repo60.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/objects/.keep b/tests/gitea-repositories-meta/user40/repo60.git/objects/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/gitea-repositories-meta/user40/repo60.git/refs/.keep b/tests/gitea-repositories-meta/user40/repo60.git/refs/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/description b/tests/gitea-repositories-meta/user5/repo4.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/tests/gitea-repositories-meta/user5/repo4.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/info/exclude b/tests/gitea-repositories-meta/user5/repo4.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/tests/gitea-repositories-meta/user5/repo4.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/tests/integration/README.md b/tests/integration/README.md
index e673bca228..3b44cfaaf0 100644
--- a/tests/integration/README.md
+++ b/tests/integration/README.md
@@ -99,18 +99,8 @@ We appreciate that some testing machines may not be very powerful and
the default timeouts for declaring a slow test or a slow clean-up flush
may not be appropriate.
-You can either:
-
-* Within the test ini file set the following section:
-
-```ini
-[integration-tests]
-SLOW_TEST = 10s ; 10s is the default value
-SLOW_FLUSH = 5S ; 5s is the default value
-```
-
-* Set the following environment variables:
+You can set the following environment variables:
```bash
-GITEA_SLOW_TEST_TIME="10s" GITEA_SLOW_FLUSH_TIME="5s" make test-sqlite
+GITEA_TEST_SLOW_RUN="10s" GITEA_TEST_SLOW_FLUSH="1s" make test-sqlite
```
diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go
index 7c231415a3..357dd27f86 100644
--- a/tests/integration/api_fork_test.go
+++ b/tests/integration/api_fork_test.go
@@ -7,8 +7,16 @@ import (
"net/http"
"testing"
+ "code.gitea.io/gitea/models"
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ org_model "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
)
func TestCreateForkNoLogin(t *testing.T) {
@@ -16,3 +24,75 @@ func TestCreateForkNoLogin(t *testing.T) {
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{})
MakeRequest(t, req, http.StatusUnauthorized)
}
+
+func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user1Sess := loginUser(t, "user1")
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+
+ // fork into a limited org
+ limitedOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
+ assert.EqualValues(t, api.VisibleTypeLimited, limitedOrg.Visibility)
+
+ ownerTeam1, err := org_model.OrgFromUser(limitedOrg).GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err)
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam1, user1))
+ user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
+ Organization: &limitedOrg.Name,
+ }).AddTokenAuth(user1Token)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ // fork into a private org
+ user4Sess := loginUser(t, "user4")
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user4"})
+ privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
+ assert.EqualValues(t, api.VisibleTypePrivate, privateOrg.Visibility)
+
+ ownerTeam2, err := org_model.OrgFromUser(privateOrg).GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err)
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user4))
+ user4Token := getTokenForLoggedInUser(t, user4Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
+ req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
+ Organization: &privateOrg.Name,
+ }).AddTokenAuth(user4Token)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var forks []*api.Repository
+ DecodeJSON(t, resp, &forks)
+
+ assert.Empty(t, forks)
+ assert.EqualValues(t, "0", resp.Header().Get("X-Total-Count"))
+ })
+
+ t.Run("Logged in", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var forks []*api.Repository
+ DecodeJSON(t, resp, &forks)
+
+ assert.Len(t, forks, 1)
+ assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count"))
+
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
+
+ req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ forks = []*api.Repository{}
+ DecodeJSON(t, resp, &forks)
+
+ assert.Len(t, forks, 2)
+ assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count"))
+ })
+}
diff --git a/tests/integration/api_repo_file_get_test.go b/tests/integration/api_repo_file_get_test.go
index 27bc9e25bf..2f897093ee 100644
--- a/tests/integration/api_repo_file_get_test.go
+++ b/tests/integration/api_repo_file_get_test.go
@@ -39,11 +39,11 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
- lfs := lfsCommitAndPushTest(t, dstPath, littleSize)[0]
+ lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
- assert.Equal(t, littleSize, respLFS.Length)
+ assert.Equal(t, testFileSizeSmall, respLFS.Length)
doAPIDeleteRepository(httpContext)
})
diff --git a/tests/integration/api_twofa_test.go b/tests/integration/api_twofa_test.go
index aad806b6dc..18e6fa91b7 100644
--- a/tests/integration/api_twofa_test.go
+++ b/tests/integration/api_twofa_test.go
@@ -53,3 +53,56 @@ func TestAPITwoFactor(t *testing.T) {
req.Header.Set("X-Gitea-OTP", passcode)
MakeRequest(t, req, http.StatusOK)
}
+
+func TestBasicAuthWithWebAuthn(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // user1 has no webauthn enrolled, he can request API with basic auth
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ unittest.AssertNotExistsBean(t, &auth_model.WebAuthnCredential{UserID: user1.ID})
+ req := NewRequest(t, "GET", "/api/v1/user")
+ req.SetBasicAuth(user1.Name, "password")
+ MakeRequest(t, req, http.StatusOK)
+
+ // user1 has no webauthn enrolled, he can request git protocol with basic auth
+ req = NewRequest(t, "GET", "/user2/repo1/info/refs")
+ req.SetBasicAuth(user1.Name, "password")
+ MakeRequest(t, req, http.StatusOK)
+
+ // user1 has no webauthn enrolled, he can request container package with basic auth
+ req = NewRequest(t, "GET", "/v2/token")
+ req.SetBasicAuth(user1.Name, "password")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type tokenResponse struct {
+ Token string `json:"token"`
+ }
+ var tokenParsed tokenResponse
+ DecodeJSON(t, resp, &tokenParsed)
+ assert.NotEmpty(t, tokenParsed.Token)
+
+ // user32 has webauthn enrolled, he can't request API with basic auth
+ user32 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32})
+ unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{UserID: user32.ID})
+
+ req = NewRequest(t, "GET", "/api/v1/user")
+ req.SetBasicAuth(user32.Name, "notpassword")
+ resp = MakeRequest(t, req, http.StatusUnauthorized)
+
+ type userResponse struct {
+ Message string `json:"message"`
+ }
+ var userParsed userResponse
+ DecodeJSON(t, resp, &userParsed)
+ assert.EqualValues(t, "Basic authorization is not allowed while webAuthn enrolled", userParsed.Message)
+
+ // user32 has webauthn enrolled, he can't request git protocol with basic auth
+ req = NewRequest(t, "GET", "/user2/repo1/info/refs")
+ req.SetBasicAuth(user32.Name, "notpassword")
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ // user32 has webauthn enrolled, he can't request container package with basic auth
+ req = NewRequest(t, "GET", "/v2/token")
+ req.SetBasicAuth(user1.Name, "notpassword")
+ MakeRequest(t, req, http.StatusUnauthorized)
+}
diff --git a/tests/integration/db_collation_test.go b/tests/integration/db_collation_test.go
index 75a4c1594f..acec4aa5d1 100644
--- a/tests/integration/db_collation_test.go
+++ b/tests/integration/db_collation_test.go
@@ -73,9 +73,12 @@ func TestDatabaseCollation(t *testing.T) {
t.Run("Convert tables to utf8mb4_bin", func(t *testing.T) {
defer test.MockVariableValue(&setting.Database.CharsetCollation, "utf8mb4_bin")()
- assert.NoError(t, db.ConvertDatabaseTable())
r, err := db.CheckCollations(x)
assert.NoError(t, err)
+ assert.EqualValues(t, "utf8mb4_bin", r.ExpectedCollation)
+ assert.NoError(t, db.ConvertDatabaseTable())
+ r, err = db.CheckCollations(x)
+ assert.NoError(t, err)
assert.Equal(t, "utf8mb4_bin", r.DatabaseCollation)
assert.True(t, r.CollationEquals(r.ExpectedCollation, r.DatabaseCollation))
assert.Empty(t, r.InconsistentCollationColumns)
diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go
index 7fd19e7edd..a47cb75196 100644
--- a/tests/integration/git_general_test.go
+++ b/tests/integration/git_general_test.go
@@ -4,9 +4,10 @@
package integration
import (
- "crypto/rand"
"encoding/hex"
"fmt"
+ "io"
+ mathRand "math/rand/v2"
"net/http"
"net/url"
"os"
@@ -34,8 +35,8 @@ import (
)
const (
- littleSize = 1024 // 1K
- bigSize = 128 * 1024 * 1024 // 128M
+ testFileSizeSmall = 10
+ testFileSizeLarge = 10 * 1024 * 1024 // 10M
)
func TestGitGeneral(t *testing.T) {
@@ -73,8 +74,8 @@ func testGitGeneral(t *testing.T, u *url.URL) {
t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
- pushedFilesStandard := standardCommitAndPushTest(t, dstPath, littleSize, bigSize)
- pushedFilesLFS := lfsCommitAndPushTest(t, dstPath, littleSize, bigSize)
+ pushedFilesStandard := standardCommitAndPushTest(t, dstPath, testFileSizeSmall, testFileSizeLarge)
+ pushedFilesLFS := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall, testFileSizeLarge)
rawTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
mediaTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
@@ -114,8 +115,8 @@ func testGitGeneral(t *testing.T, u *url.URL) {
t.Run("Clone", doGitClone(dstPath, sshURL))
- pushedFilesStandard := standardCommitAndPushTest(t, dstPath, littleSize, bigSize)
- pushedFilesLFS := lfsCommitAndPushTest(t, dstPath, littleSize, bigSize)
+ pushedFilesStandard := standardCommitAndPushTest(t, dstPath, testFileSizeSmall, testFileSizeLarge)
+ pushedFilesLFS := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall, testFileSizeLarge)
rawTest(t, &sshContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
mediaTest(t, &sshContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
@@ -202,14 +203,14 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s
// Request raw paths
req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little))
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
- assert.Equal(t, littleSize, resp.Length)
+ assert.Equal(t, testFileSizeSmall, resp.Length)
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
resp := session.MakeRequest(t, req, http.StatusOK)
- assert.NotEqual(t, littleSize, resp.Body.Len())
+ assert.NotEqual(t, testFileSizeSmall, resp.Body.Len())
assert.LessOrEqual(t, resp.Body.Len(), 1024)
- if resp.Body.Len() != littleSize && resp.Body.Len() <= 1024 {
+ if resp.Body.Len() != testFileSizeSmall && resp.Body.Len() <= 1024 {
assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
}
}
@@ -217,13 +218,13 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s
if !testing.Short() {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big))
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
- assert.Equal(t, bigSize, resp.Length)
+ assert.Equal(t, testFileSizeLarge, resp.Length)
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS))
resp := session.MakeRequest(t, req, http.StatusOK)
- assert.NotEqual(t, bigSize, resp.Body.Len())
- if resp.Body.Len() != bigSize && resp.Body.Len() <= 1024 {
+ assert.NotEqual(t, testFileSizeLarge, resp.Body.Len())
+ if resp.Body.Len() != testFileSizeLarge && resp.Body.Len() <= 1024 {
assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
}
}
@@ -243,21 +244,21 @@ func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS
// Request media paths
req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little))
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
- assert.Equal(t, littleSize, resp.Length)
+ assert.Equal(t, testFileSizeSmall, resp.Length)
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
- assert.Equal(t, littleSize, resp.Length)
+ assert.Equal(t, testFileSizeSmall, resp.Length)
if !testing.Short() {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
- assert.Equal(t, bigSize, resp.Length)
+ assert.Equal(t, testFileSizeLarge, resp.Length)
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
- assert.Equal(t, bigSize, resp.Length)
+ assert.Equal(t, testFileSizeLarge, resp.Length)
}
}
})
@@ -287,35 +288,19 @@ func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string {
}
func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) {
- // Generate random file
- bufSize := 4 * 1024
- if bufSize > size {
- bufSize = size
- }
-
- buffer := make([]byte, bufSize)
-
tmpFile, err := os.CreateTemp(repoPath, prefix)
if err != nil {
return "", err
}
defer tmpFile.Close()
- written := 0
- for written < size {
- n := size - written
- if n > bufSize {
- n = bufSize
- }
- _, err := rand.Read(buffer[:n])
- if err != nil {
- return "", err
- }
- n, err = tmpFile.Write(buffer[:n])
- if err != nil {
- return "", err
- }
- written += n
+
+ var seed [32]byte
+ rander := mathRand.NewChaCha8(seed) // for testing only, no need to seed
+ _, err = io.CopyN(tmpFile, rander, int64(size))
+ if err != nil {
+ return "", err
}
+ _ = tmpFile.Close()
// Commit
// Now here we should explicitly allow lfs filters to run
@@ -355,7 +340,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
// Try to push without permissions, which should fail
t.Run("TryPushWithoutPermissions", func(t *testing.T) {
- _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ _, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
doGitPushTestRepositoryFail(dstPath, "origin", "protected")
})
@@ -367,7 +352,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
// Normal push should work
t.Run("NormalPushWithPermissions", func(t *testing.T) {
- _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ _, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
doGitPushTestRepository(dstPath, "origin", "protected")
})
@@ -376,7 +361,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
t.Run("ForcePushWithoutForcePermissions", func(t *testing.T) {
t.Run("CreateDivergentHistory", func(t *testing.T) {
git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath})
- _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-new")
+ _, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-new")
assert.NoError(t, err)
})
doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
@@ -411,7 +396,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
assert.NoError(t, err)
})
t.Run("GenerateCommit", func(t *testing.T) {
- _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ _, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
})
t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2"))
@@ -426,7 +411,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*"))
t.Run("GenerateCommit", func(t *testing.T) {
- _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-")
+ _, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "unprotected-file-")
assert.NoError(t, err)
})
t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
@@ -436,7 +421,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master"))
t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce"))
t.Run("GenerateCommit", func(t *testing.T) {
- _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ _, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
})
t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected"))
@@ -649,7 +634,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected"))
t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
t.Run("GenerateCommit", func(t *testing.T) {
- _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ _, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
})
t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected3"))
diff --git a/tests/integration/git_misc_test.go b/tests/integration/git_misc_test.go
index 82ab184bb0..00ed2a766f 100644
--- a/tests/integration/git_misc_test.go
+++ b/tests/integration/git_misc_test.go
@@ -98,7 +98,7 @@ func TestAgitPullPush(t *testing.T) {
doGitCreateBranch(dstPath, "test-agit-push")
// commit 1
- _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ _, err = generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
// push to create an agit pull request
@@ -115,7 +115,7 @@ func TestAgitPullPush(t *testing.T) {
assert.Equal(t, "test-description", pr.Issue.Content)
// commit 2
- _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-2-")
+ _, err = generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-2-")
assert.NoError(t, err)
// push 2
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index f72ac5f51c..4338e19617 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -20,7 +20,6 @@ import (
"strings"
"sync/atomic"
"testing"
- "time"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
@@ -28,7 +27,6 @@ import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/testlogger"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers"
@@ -90,27 +88,6 @@ func TestMain(m *testing.M) {
tests.InitTest(true)
testWebRoutes = routers.NormalRoutes()
- // integration test settings...
- if setting.CfgProvider != nil {
- testingCfg := setting.CfgProvider.Section("integration-tests")
- testlogger.SlowTest = testingCfg.Key("SLOW_TEST").MustDuration(testlogger.SlowTest)
- testlogger.SlowFlush = testingCfg.Key("SLOW_FLUSH").MustDuration(testlogger.SlowFlush)
- }
-
- if os.Getenv("GITEA_SLOW_TEST_TIME") != "" {
- duration, err := time.ParseDuration(os.Getenv("GITEA_SLOW_TEST_TIME"))
- if err == nil {
- testlogger.SlowTest = duration
- }
- }
-
- if os.Getenv("GITEA_SLOW_FLUSH_TIME") != "" {
- duration, err := time.ParseDuration(os.Getenv("GITEA_SLOW_FLUSH_TIME"))
- if err == nil {
- testlogger.SlowFlush = duration
- }
- }
-
os.Unsetenv("GIT_AUTHOR_NAME")
os.Unsetenv("GIT_AUTHOR_EMAIL")
os.Unsetenv("GIT_AUTHOR_DATE")
@@ -132,8 +109,6 @@ func TestMain(m *testing.M) {
// Instead, "No tests were found", last nonsense log is "According to the configuration, subsequent logs will not be printed to the console"
exitCode := m.Run()
- testlogger.WriterCloser.Reset()
-
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
fmt.Printf("util.RemoveAll: %v\n", err)
os.Exit(1)
@@ -400,8 +375,9 @@ func MakeRequest(t testing.TB, rw *RequestWrapper, expectedStatus int) *httptest
}
testWebRoutes.ServeHTTP(recorder, req)
if expectedStatus != NoExpectedStatus {
- if !assert.EqualValues(t, expectedStatus, recorder.Code, "Request: %s %s", req.Method, req.URL.String()) {
+ if expectedStatus != recorder.Code {
logUnexpectedResponse(t, recorder)
+ require.Equal(t, expectedStatus, recorder.Code, "Request: %s %s", req.Method, req.URL.String())
}
}
return recorder
diff --git a/tests/integration/linguist_test.go b/tests/integration/linguist_test.go
index e569de93a8..2d50dc599a 100644
--- a/tests/integration/linguist_test.go
+++ b/tests/integration/linguist_test.go
@@ -6,6 +6,7 @@ package integration
import (
"context"
"net/url"
+ "strconv"
"strings"
"testing"
"time"
@@ -19,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/queue"
repo_service "code.gitea.io/gitea/services/repository"
files_service "code.gitea.io/gitea/services/repository/files"
+ "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
@@ -218,42 +220,43 @@ func TestLinguist(t *testing.T) {
}
for i, c := range cases {
- repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
- Name: "linguist-test",
+ t.Run("Case-"+strconv.Itoa(i), func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
+ Name: "linguist-test-" + strconv.Itoa(i),
+ })
+ assert.NoError(t, err)
+
+ files := []*files_service.ChangeRepoFile{
+ {
+ TreePath: ".gitattributes",
+ ContentReader: strings.NewReader(c.GitAttributesContent),
+ },
+ }
+ files = append(files, c.FilesToAdd...)
+ for _, f := range files {
+ f.Operation = "create"
+ }
+
+ _, err = files_service.ChangeRepoFiles(git.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
+ Files: files,
+ OldBranch: repo.DefaultBranch,
+ NewBranch: repo.DefaultBranch,
+ })
+ assert.NoError(t, err)
+
+ assert.NoError(t, stats.UpdateRepoIndexer(repo))
+ assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 10*time.Second))
+
+ stats, err := repo_model.GetTopLanguageStats(db.DefaultContext, repo, len(c.FilesToAdd))
+ assert.NoError(t, err)
+
+ languages := make([]string, 0, len(stats))
+ for _, s := range stats {
+ languages = append(languages, s.Language)
+ }
+ assert.Equal(t, c.ExpectedLanguageOrder, languages, "case %d: unexpected language stats", i)
})
- assert.NoError(t, err)
-
- files := []*files_service.ChangeRepoFile{
- {
- TreePath: ".gitattributes",
- ContentReader: strings.NewReader(c.GitAttributesContent),
- },
- }
- files = append(files, c.FilesToAdd...)
- for _, f := range files {
- f.Operation = "create"
- }
-
- _, err = files_service.ChangeRepoFiles(git.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
- Files: files,
- OldBranch: repo.DefaultBranch,
- NewBranch: repo.DefaultBranch,
- })
- assert.NoError(t, err)
-
- assert.NoError(t, stats.UpdateRepoIndexer(repo))
- assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 10*time.Second))
-
- stats, err := repo_model.GetTopLanguageStats(db.DefaultContext, repo, len(c.FilesToAdd))
- assert.NoError(t, err)
-
- languages := make([]string, 0, len(stats))
- for _, s := range stats {
- languages = append(languages, s.Language)
- }
- assert.Equal(t, c.ExpectedLanguageOrder, languages, "case %d: unexpected language stats", i)
-
- assert.NoError(t, repo_service.DeleteRepository(db.DefaultContext, user, repo, false))
}
})
}
diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go
index 40fcf95705..627d1f89c4 100644
--- a/tests/integration/migration-test/migration_test.go
+++ b/tests/integration/migration-test/migration_test.go
@@ -28,33 +28,29 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/testlogger"
"code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/tests"
-
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"xorm.io/xorm"
)
var currentEngine *xorm.Engine
func initMigrationTest(t *testing.T) func() {
- log.RegisterEventWriter("test", testlogger.NewTestLoggerWriter)
+ testlogger.Init()
- deferFn := tests.PrintCurrentTest(t, 2)
+ deferFn := testlogger.PrintCurrentTest(t, 2)
giteaRoot := base.SetupGiteaRoot()
if giteaRoot == "" {
- tests.Printf("Environment variable $GITEA_ROOT not set\n")
- os.Exit(1)
+ testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n")
}
setting.AppPath = path.Join(giteaRoot, "gitea")
if _, err := os.Stat(setting.AppPath); err != nil {
- tests.Printf("Could not find gitea binary at %s\n", setting.AppPath)
- os.Exit(1)
+ testlogger.Fatalf(fmt.Sprintf("Could not find gitea binary at %s\n", setting.AppPath))
}
giteaConf := os.Getenv("GITEA_CONF")
if giteaConf == "" {
- tests.Printf("Environment variable $GITEA_CONF not set\n")
- os.Exit(1)
+ testlogger.Fatalf("Environment variable $GITEA_CONF not set\n")
} else if !path.IsAbs(giteaConf) {
setting.CustomConf = path.Join(giteaRoot, giteaConf)
} else {
@@ -64,28 +60,7 @@ func initMigrationTest(t *testing.T) func() {
unittest.InitSettings()
assert.True(t, len(setting.RepoRootPath) != 0)
- assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
- assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
- ownerDirs, err := os.ReadDir(setting.RepoRootPath)
- if err != nil {
- assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
- }
- for _, ownerDir := range ownerDirs {
- if !ownerDir.Type().IsDir() {
- continue
- }
- repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
- if err != nil {
- assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
- }
- for _, repoDir := range repoDirs {
- _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
- _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
- _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
- _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
- }
- }
-
+ assert.NoError(t, unittest.SyncDirs(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
assert.NoError(t, git.InitFull(context.Background()))
setting.LoadDBSetting()
setting.InitLoggersForTest()
@@ -144,13 +119,10 @@ func readSQLFromFile(version string) (string, error) {
return string(charset.MaybeRemoveBOM(bytes, charset.ConvertOpts{})), nil
}
-func restoreOldDB(t *testing.T, version string) bool {
+func restoreOldDB(t *testing.T, version string) {
data, err := readSQLFromFile(version)
- assert.NoError(t, err)
- if len(data) == 0 {
- tests.Printf("No db found to restore for %s version: %s\n", setting.Database.Type, version)
- return false
- }
+ require.NoError(t, err)
+ require.NotEmpty(t, data, "No data found for %s version: %s", setting.Database.Type, version)
switch {
case setting.Database.Type.IsSQLite3():
@@ -218,15 +190,12 @@ func restoreOldDB(t *testing.T, version string) bool {
db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
}
- if !assert.NoError(t, err) {
- return false
- }
+ require.NoError(t, err)
defer db.Close()
schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema))
- if !assert.NoError(t, err) || !assert.NotEmpty(t, schrows) {
- return false
- }
+ require.NoError(t, err)
+ require.NotEmpty(t, schrows)
if !schrows.Next() {
// Create and setup a DB schema
@@ -281,7 +250,6 @@ func restoreOldDB(t *testing.T, version string) bool {
}
db.Close()
}
- return true
}
func wrappedMigrate(x *xorm.Engine) error {
@@ -290,11 +258,8 @@ func wrappedMigrate(x *xorm.Engine) error {
}
func doMigrationTest(t *testing.T, version string) {
- defer tests.PrintCurrentTest(t)()
- tests.Printf("Performing migration test for %s version: %s\n", setting.Database.Type, version)
- if !restoreOldDB(t, version) {
- return
- }
+ defer testlogger.PrintCurrentTest(t)()
+ restoreOldDB(t, version)
setting.InitSQLLoggersForCli(log.INFO)
@@ -326,14 +291,9 @@ func TestMigrations(t *testing.T) {
dialect := setting.Database.Type
versions, err := availableVersions()
- assert.NoError(t, err)
+ require.NoError(t, err)
+ require.NotEmpty(t, versions, "No old database versions available to migration test for %s", dialect)
- if len(versions) == 0 {
- tests.Printf("No old database versions available to migration test for %s\n", dialect)
- return
- }
-
- tests.Printf("Preparing to test %d migrations for %s\n", len(versions), dialect)
for _, version := range versions {
t.Run(fmt.Sprintf("Migrate-%s-%s", dialect, version), func(t *testing.T) {
doMigrationTest(t, version)
diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go
index 6b1c808cf4..9ff4669bef 100644
--- a/tests/integration/mirror_push_test.go
+++ b/tests/integration/mirror_push_test.go
@@ -9,7 +9,9 @@ import (
"net/http"
"net/url"
"strconv"
+ "strings"
"testing"
+ "time"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
@@ -32,11 +34,10 @@ func TestMirrorPush(t *testing.T) {
}
func testMirrorPush(t *testing.T, u *url.URL) {
- defer tests.PrepareTestEnv(t)()
-
setting.Migrations.AllowLocalNetworks = true
assert.NoError(t, migrations.Init())
+ _ = db.TruncateBeans(db.DefaultContext, &repo_model.PushMirror{})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
@@ -45,9 +46,10 @@ func testMirrorPush(t *testing.T, u *url.URL) {
})
assert.NoError(t, err)
- ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name)
+ session := loginUser(t, user.Name)
- doCreatePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape(mirrorRepo.Name)), user.LowerName, userPassword)(t)
+ pushMirrorURL := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo.Name))
+ testCreatePushMirror(t, session, user.Name, srcRepo.Name, pushMirrorURL, user.LowerName, userPassword, "0")
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
assert.NoError(t, err)
@@ -73,49 +75,81 @@ func testMirrorPush(t *testing.T, u *url.URL) {
assert.Equal(t, srcCommit.ID, mirrorCommit.ID)
// Cleanup
- doRemovePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape(mirrorRepo.Name)), user.LowerName, userPassword, int(mirrors[0].ID))(t)
+ assert.True(t, doRemovePushMirror(t, session, user.Name, srcRepo.Name, mirrors[0].ID))
mirrors, _, err = repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
assert.NoError(t, err)
assert.Len(t, mirrors, 0)
}
-func doCreatePushMirror(ctx APITestContext, address, username, password string) func(t *testing.T) {
- return func(t *testing.T) {
- csrf := GetUserCSRFToken(t, ctx.Session)
+func testCreatePushMirror(t *testing.T, session *TestSession, owner, repo, address, username, password, interval string) {
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(owner), url.PathEscape(repo)), map[string]string{
+ "_csrf": GetUserCSRFToken(t, session),
+ "action": "push-mirror-add",
+ "push_mirror_address": address,
+ "push_mirror_username": username,
+ "push_mirror_password": password,
+ "push_mirror_interval": interval,
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
- req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
- "_csrf": csrf,
- "action": "push-mirror-add",
- "push_mirror_address": address,
- "push_mirror_username": username,
- "push_mirror_password": password,
- "push_mirror_interval": "0",
- })
- ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
-
- flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash)
- assert.NotNil(t, flashCookie)
- assert.Contains(t, flashCookie.Value, "success")
- }
+ flashCookie := session.GetCookie(gitea_context.CookieNameFlash)
+ assert.NotNil(t, flashCookie)
+ assert.Contains(t, flashCookie.Value, "success")
}
-func doRemovePushMirror(ctx APITestContext, address, username, password string, pushMirrorID int) func(t *testing.T) {
- return func(t *testing.T) {
- csrf := GetUserCSRFToken(t, ctx.Session)
-
- req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
- "_csrf": csrf,
- "action": "push-mirror-remove",
- "push_mirror_id": strconv.Itoa(pushMirrorID),
- "push_mirror_address": address,
- "push_mirror_username": username,
- "push_mirror_password": password,
- "push_mirror_interval": "0",
- })
- ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
-
- flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash)
- assert.NotNil(t, flashCookie)
- assert.Contains(t, flashCookie.Value, "success")
- }
+func doRemovePushMirror(t *testing.T, session *TestSession, owner, repo string, pushMirrorID int64) bool {
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(owner), url.PathEscape(repo)), map[string]string{
+ "_csrf": GetUserCSRFToken(t, session),
+ "action": "push-mirror-remove",
+ "push_mirror_id": strconv.FormatInt(pushMirrorID, 10),
+ })
+ resp := session.MakeRequest(t, req, NoExpectedStatus)
+ flashCookie := session.GetCookie(gitea_context.CookieNameFlash)
+ return resp.Code == http.StatusSeeOther && flashCookie != nil && strings.Contains(flashCookie.Value, "success")
+}
+
+func doUpdatePushMirror(t *testing.T, session *TestSession, owner, repo string, pushMirrorID int64, interval string) bool {
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", owner, repo), map[string]string{
+ "_csrf": GetUserCSRFToken(t, session),
+ "action": "push-mirror-update",
+ "push_mirror_id": strconv.FormatInt(pushMirrorID, 10),
+ "push_mirror_interval": interval,
+ "push_mirror_defer_sync": "true",
+ })
+ resp := session.MakeRequest(t, req, NoExpectedStatus)
+ return resp.Code == http.StatusSeeOther
+}
+
+func TestRepoSettingPushMirrorUpdate(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ setting.Migrations.AllowLocalNetworks = true
+ assert.NoError(t, migrations.Init())
+
+ session := loginUser(t, "user2")
+ repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ testCreatePushMirror(t, session, "user2", "repo2", "https://127.0.0.1/user1/repo1.git", "", "", "24h")
+
+ pushMirrors, cnt, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, repo2.ID, db.ListOptions{})
+ assert.NoError(t, err)
+ assert.EqualValues(t, 1, cnt)
+ assert.EqualValues(t, 24*time.Hour, pushMirrors[0].Interval)
+ repo2PushMirrorID := pushMirrors[0].ID
+
+ // update repo2 push mirror
+ assert.True(t, doUpdatePushMirror(t, session, "user2", "repo2", repo2PushMirrorID, "10m0s"))
+ pushMirror := unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: repo2PushMirrorID})
+ assert.EqualValues(t, 10*time.Minute, pushMirror.Interval)
+
+ // avoid updating repo2 push mirror from repo1
+ assert.False(t, doUpdatePushMirror(t, session, "user2", "repo1", repo2PushMirrorID, "20m0s"))
+ pushMirror = unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: repo2PushMirrorID})
+ assert.EqualValues(t, 10*time.Minute, pushMirror.Interval) // not changed
+
+ // avoid deleting repo2 push mirror from repo1
+ assert.False(t, doRemovePushMirror(t, session, "user2", "repo1", repo2PushMirrorID))
+ unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: repo2PushMirrorID})
+
+ // delete repo2 push mirror
+ assert.True(t, doRemovePushMirror(t, session, "user2", "repo2", repo2PushMirrorID))
+ unittest.AssertNotExistsBean(t, &repo_model.PushMirror{ID: repo2PushMirrorID})
}
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
index 6d1cc8afcf..2b4c417334 100644
--- a/tests/integration/repo_branch_test.go
+++ b/tests/integration/repo_branch_test.go
@@ -209,8 +209,6 @@ func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath
}
func TestRecentlyPushedNewBranches(t *testing.T) {
- defer tests.PrepareTestEnv(t)()
-
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user1Session := loginUser(t, "user1")
user2Session := loginUser(t, "user2")
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index feebebf062..52b55888b9 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -9,8 +9,12 @@ import (
"net/http/httptest"
"testing"
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ org_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -74,3 +78,51 @@ func TestRepoForkToOrg(t *testing.T) {
_, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href")
assert.False(t, exists, "Forking should not be allowed anymore")
}
+
+func TestForkListLimitedAndPrivateRepos(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ forkItemSelector := ".repo-fork-item"
+
+ user1Sess := loginUser(t, "user1")
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+
+ // fork to a limited org
+ limitedOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
+ assert.EqualValues(t, structs.VisibleTypeLimited, limitedOrg.Visibility)
+ ownerTeam1, err := org_model.OrgFromUser(limitedOrg).GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err)
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam1, user1))
+ testRepoFork(t, user1Sess, "user2", "repo1", limitedOrg.Name, "repo1", "")
+
+ // fork to a private org
+ user4Sess := loginUser(t, "user4")
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user4"})
+ privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
+ assert.EqualValues(t, structs.VisibleTypePrivate, privateOrg.Visibility)
+ ownerTeam2, err := org_model.OrgFromUser(privateOrg).GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err)
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user4))
+ testRepoFork(t, user4Sess, "user2", "repo1", privateOrg.Name, "repo1", "")
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ req := NewRequest(t, "GET", "/user2/repo1/forks")
+ resp := MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.EqualValues(t, 0, htmlDoc.Find(forkItemSelector).Length())
+ })
+
+ t.Run("Logged in", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/forks")
+ resp := user1Sess.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.EqualValues(t, 1, htmlDoc.Find(forkItemSelector).Length())
+
+ assert.NoError(t, models.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
+ resp = user1Sess.MakeRequest(t, req, http.StatusOK)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ assert.EqualValues(t, 2, htmlDoc.Find(forkItemSelector).Length())
+ })
+}
diff --git a/tests/test_utils.go b/tests/test_utils.go
index 3503ca1975..deefdd43c5 100644
--- a/tests/test_utils.go
+++ b/tests/test_utils.go
@@ -29,17 +29,12 @@ import (
"github.com/stretchr/testify/assert"
)
-func exitf(format string, args ...any) {
- fmt.Printf(format+"\n", args...)
- os.Exit(1)
-}
-
func InitTest(requireGitea bool) {
- log.RegisterEventWriter("test", testlogger.NewTestLoggerWriter)
+ testlogger.Init()
giteaRoot := base.SetupGiteaRoot()
if giteaRoot == "" {
- exitf("Environment variable $GITEA_ROOT not set")
+ testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n")
}
// TODO: Speedup tests that rely on the event source ticker, confirm whether there is any bug or failure.
@@ -54,7 +49,7 @@ func InitTest(requireGitea bool) {
}
setting.AppPath = filepath.Join(giteaRoot, giteaBinary)
if _, err := os.Stat(setting.AppPath); err != nil {
- exitf("Could not find gitea binary at %s", setting.AppPath)
+ testlogger.Fatalf("Could not find gitea binary at %s\n", setting.AppPath)
}
}
giteaConf := os.Getenv("GITEA_CONF")
@@ -66,7 +61,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 {
- exitf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify`)
+ testlogger.Fatalf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify` + "\n")
}
}
if !filepath.IsAbs(giteaConf) {
@@ -85,7 +80,7 @@ func InitTest(requireGitea bool) {
setting.LoadDBSetting()
if err := storage.Init(); err != nil {
- exitf("Init storage failed: %v", err)
+ testlogger.Fatalf("Init storage failed: %v\n", err)
}
switch {
@@ -195,30 +190,7 @@ func PrepareGitRepoDirectory(t testing.TB) {
if !assert.NotEmpty(t, setting.RepoRootPath) {
return
}
-
- assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
- assert.NoError(t, unittest.CopyDir(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
-
- ownerDirs, err := os.ReadDir(setting.RepoRootPath)
- if err != nil {
- assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
- }
- for _, ownerDir := range ownerDirs {
- if !ownerDir.Type().IsDir() {
- continue
- }
- repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
- if err != nil {
- assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
- }
- for _, repoDir := range repoDirs {
- _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
- _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
- _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
- _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
- _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "pull"), 0o755)
- }
- }
+ assert.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
}
func PrepareArtifactsStorage(t testing.TB) {
@@ -281,8 +253,3 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() {
t.Helper()
return testlogger.PrintCurrentTest(t, util.OptionalArg(skip)+1)
}
-
-// Printf takes a format and args and prints the string to os.Stdout
-func Printf(format string, args ...any) {
- testlogger.Printf(format, args...)
-}
diff --git a/web_src/css/modules/comment.css b/web_src/css/modules/comment.css
index 68306686ef..9947b15b9a 100644
--- a/web_src/css/modules/comment.css
+++ b/web_src/css/modules/comment.css
@@ -53,6 +53,7 @@
display: flex;
flex-direction: column;
flex: 1;
+ min-width: 0;
}
.ui.comments .comment > .avatar ~ .content {
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 986fcc1181..3d3ac2fc69 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -1,8 +1,8 @@