diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 429829dfe0..7e988e0449 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -37,7 +37,7 @@ jobs: python-version: "3.12" - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: pip install poetry @@ -66,7 +66,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend @@ -137,7 +137,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend @@ -186,7 +186,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend 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/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index 35ac7598f6..b84c69e4a0 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -23,7 +23,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend frontend deps-backend diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 10fe94b296..6e1b6e0758 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -22,7 +22,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend deps-backend diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index 55908d3657..41037df29c 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -23,7 +23,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend deps-backend diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index edf7ea1270..a23e698200 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -25,7 +25,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend deps-backend 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/flake.lock b/flake.lock index 9eadad2b94..1890b82dcf 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1720542800, - "narHash": "sha256-ZgnNHuKV6h2+fQ5LuqnUaqZey1Lqqt5dTUAiAnqH0QQ=", + "lastModified": 1731139594, + "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "feb2849fdeb70028c70d73b848214b00d324a497", + "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e2f273e341..2c9d74c137 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,7 @@ gzip # frontend - nodejs_20 + nodejs_22 # linting python312 diff --git a/models/actions/run.go b/models/actions/run.go index 37064520a2..732fb48bb9 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -261,6 +261,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin } // InsertRun inserts a run +// The title will be cut off at 255 characters if it's longer than 255 characters. func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { ctx, committer, err := db.TxContext(ctx) if err != nil { @@ -273,6 +274,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork return err } run.Index = index + run.Title, _ = util.SplitStringAtByteN(run.Title, 255) if err := db.Insert(ctx, run); err != nil { return err @@ -399,6 +401,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { if len(cols) > 0 { sess.Cols(cols...) } + run.Title, _ = util.SplitStringAtByteN(run.Title, 255) affected, err := sess.Update(run) if err != nil { return err diff --git a/models/actions/runner.go b/models/actions/runner.go index 2023ba4f99..b35a76680c 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -252,6 +252,7 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) { // UpdateRunner updates runner's information. func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { e := db.GetEngine(ctx) + r.Name, _ = util.SplitStringAtByteN(r.Name, 255) var err error if len(cols) == 0 { _, err = e.ID(r.ID).AllCols().Update(r) @@ -278,6 +279,7 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error { // Remove OwnerID to avoid confusion; it's not worth returning an error here. t.OwnerID = 0 } + t.Name, _ = util.SplitStringAtByteN(t.Name, 255) return db.Insert(ctx, t) } diff --git a/models/actions/schedule.go b/models/actions/schedule.go index c751ef51ca..961ffd0851 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -67,6 +68,7 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { // Loop through each schedule row for _, row := range rows { + row.Title, _ = util.SplitStringAtByteN(row.Title, 255) // Create new schedule row if err = db.Insert(ctx, row); err != nil { return err diff --git a/models/activities/action.go b/models/activities/action.go index c83dba9d46..43da557fff 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -251,6 +251,9 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string { // GetRepoUserName returns the name of the action repository owner. func (a *Action) GetRepoUserName(ctx context.Context) string { a.loadRepo(ctx) + if a.Repo == nil { + return "(non-existing-repo)" + } return a.Repo.OwnerName } @@ -263,6 +266,9 @@ func (a *Action) ShortRepoUserName(ctx context.Context) string { // GetRepoName returns the name of the action repository. func (a *Action) GetRepoName(ctx context.Context) string { a.loadRepo(ctx) + if a.Repo == nil { + return "(non-existing-repo)" + } return a.Repo.Name } diff --git a/models/activities/notification.go b/models/activities/notification.go index b888adeb60..6dde26fd53 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" + "xorm.io/xorm/schemas" ) type ( @@ -50,25 +51,64 @@ const ( // Notification represents a notification type Notification struct { ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"INDEX NOT NULL"` - RepoID int64 `xorm:"INDEX NOT NULL"` + UserID int64 `xorm:"NOT NULL"` + RepoID int64 `xorm:"NOT NULL"` - Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"` - Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"` + Status NotificationStatus `xorm:"SMALLINT NOT NULL"` + Source NotificationSource `xorm:"SMALLINT NOT NULL"` - IssueID int64 `xorm:"INDEX NOT NULL"` - CommitID string `xorm:"INDEX"` + IssueID int64 `xorm:"NOT NULL"` + CommitID string CommentID int64 - UpdatedBy int64 `xorm:"INDEX NOT NULL"` + UpdatedBy int64 `xorm:"NOT NULL"` Issue *issues_model.Issue `xorm:"-"` Repository *repo_model.Repository `xorm:"-"` Comment *issues_model.Comment `xorm:"-"` User *user_model.User `xorm:"-"` - CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"` - UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"` +} + +// TableIndices implements xorm's TableIndices interface +func (n *Notification) TableIndices() []*schemas.Index { + indices := make([]*schemas.Index, 0, 8) + usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType) + usuuIndex.AddColumn("user_id", "status", "updated_unix") + indices = append(indices, usuuIndex) + + // Add the individual indices that were previously defined in struct tags + userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType) + userIDIndex.AddColumn("user_id") + indices = append(indices, userIDIndex) + + repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType) + repoIDIndex.AddColumn("repo_id") + indices = append(indices, repoIDIndex) + + statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType) + statusIndex.AddColumn("status") + indices = append(indices, statusIndex) + + sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType) + sourceIndex.AddColumn("source") + indices = append(indices, sourceIndex) + + issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType) + issueIDIndex.AddColumn("issue_id") + indices = append(indices, issueIDIndex) + + commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType) + commitIDIndex.AddColumn("commit_id") + indices = append(indices, commitIDIndex) + + updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType) + updatedByIndex.AddColumn("updated_by") + indices = append(indices, updatedByIndex) + + return indices } func init() { 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/issues/issue_update.go b/models/issues/issue_update.go index 31d76be5e0..5b929c9115 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/references" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -138,6 +139,7 @@ func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, } defer committer.Close() + issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = UpdateIssueCols(ctx, issue, "name"); err != nil { return fmt.Errorf("updateIssueCols: %w", err) } @@ -386,6 +388,7 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue } // NewIssue creates new issue with labels for repository. +// The title will be cut off at 255 characters if it's longer than 255 characters. func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { @@ -399,6 +402,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la } issue.Index = idx + issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, diff --git a/models/issues/pull.go b/models/issues/pull.go index 4475ab3a4f..853e2a69e6 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -572,6 +572,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss } issue.Index = idx + issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, 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/migrations/migrations.go b/models/migrations/migrations.go index 50cda3e68a..5f30a55317 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -350,6 +350,7 @@ func prepareMigrationTasks() []*xormigrate.Migration { newMigration(306, "Add BlockAdminMergeOverride to ProtectedBranch", v1_23.AddBlockAdminMergeOverrideBranchProtection), newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate), newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard), + newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices), } return preparedMigrations } diff --git a/models/migrations/v1_23/v309.go b/models/migrations/v1_23/v309.go index 05b052cc49..73c35a4528 100644 --- a/models/migrations/v1_23/v309.go +++ b/models/migrations/v1_23/v309.go @@ -4,304 +4,75 @@ package v1_23 //nolint import ( - "fmt" + "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - - "src.techknowlogick.com/xormigrate" "xorm.io/xorm" + "xorm.io/xorm/schemas" ) -const ( - minDBVersion = 70 // Gitea 1.5.3 - oldMigrationsCount = 230 - expectedVersion = minDBVersion + oldMigrationsCount -) +type improveNotificationTableIndicesAction struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL"` + RepoID int64 `xorm:"NOT NULL"` -var oldMigrationNames = []string{ - "add issue_dependencies", - "protect each scratch token", - "add review", - "add must_change_password column for users table", - "add approval whitelists to protected branches", - "clear nonused data which not deleted when user was deleted", - "add pull request rebase with merge commit", - "add theme to users", - "rename repo is_bare to repo is_empty", - "add can close issues via commit in any branch", - "add is locked to issues", - "update U2F counter type", - "hot fix for wrong release sha1 on release table", - "add uploader id for table attachment", - "add table to store original imported gpg keys", - "hash application token", - "add http method to webhook", - "add avatar field to repository", - "add commit status context field to commit_status", - "add original author/url migration info to issues, comments, and repo ", - "change length of some repository columns", - "add index on owner_id of repository and type, review_id of comment", - "remove orphaned repository index statuses", - "add email notification enabled preference to user", - "add enable_status_check, status_check_contexts to protected_branch", - "add table columns for cross referencing issues", - "delete orphaned attachments", - "add repo_admin_change_team_access to user", - "add original author name and id on migrated release", - "add task table and status column for repository table", - "update migration repositories' service type", - "change length of some external login users columns", - "update migration repositories' service type v2", - "Add WhitelistDeployKeys to protected branch", - "remove unnecessary columns from label", - "add includes_all_repositories to teams", - "add column `mode` to table watch", - "Add template options to repository", - "Add comment_id on table notification", - "add can_create_org_repo to team", - "change review content type to text", - "update branch protection for can push and whitelist enable", - "remove release attachments which repository deleted", - "new feature: change target branch of pull requests", - "Remove authentication credentials from stored URL", - "add user_id prefix to existing user avatar name", - "Extend TrackedTimes", - "Add block on rejected reviews branch protection", - "Add commit id and stale to reviews", - "Fix migrated repositories' git service type", - "Add owner_name on table repository", - "add is_restricted column for users table", - "Add Require Signed Commits to ProtectedBranch", - "Add original information for reactions", - "Add columns to user and repository", - "Add some columns on review for migration", - "Fix topic repository count", - "add repository code language statistics", - "fix merge base for pull requests", - "remove dependencies from deleted repositories", - "Expand webhooks for more granularity", - "Add IsSystemWebhook column to webhooks table", - "Add Branch Protection Protected Files Column", - "Add EmailHash Table", - "Refix merge base for merged pull requests", - "Add OrgID column to Labels table", - "Add CommitsAhead and CommitsBehind Column to PullRequest Table", - "Add Branch Protection Block Outdated Branch", - "Add ResolveDoerID to Comment table", - "prepend refs/heads/ to issue refs", - "Save detected language file size to database instead of percent", - "Add KeepActivityPrivate to User table", - "Ensure Repository.IsArchived is not null", - "recalculate Stars number for all user", - "update Matrix Webhook http method to 'PUT'", - "Increase Language field to 50 in LanguageStats", - "Add projects info to repository table", - "create review for 0 review id code comments", - "remove issue dependency comments who refer to non existing issues", - "Add Created and Updated to Milestone table", - "add primary key to repo_topic", - "set default password algorithm to Argon2", - "add TrustModel field to Repository", - "add Team review request support", - "add timestamps to Star, Label, Follow, Watch and Collaboration", - "add changed_protected_files column for pull_request table", - "fix publisher ID for tag releases", - "ensure repo topics are up-to-date", - "code comment replies should have the commitID of the review they are replying to", - "update reactions constraint", - "Add block on official review requests branch protection", - "Convert task type from int to string", - "Convert webhook task type from int to string", - "Convert topic name from 25 to 50", - "Add scope and nonce columns to oauth2_grant table", - "Convert hook task type from char(16) to varchar(16) and trim the column", - "Where Password is Valid with Empty String delete it", - "Add user redirect", - "Recreate user table to fix default values", - "Update DeleteBranch comments to set the old_ref to the commit_sha", - "Add Dismissed to Review table", - "Add Sorting to ProjectBoard table", - "Add sessions table for go-chi/session", - "Add time_id column to Comment", - "Create repo transfer table", - "Fix Postgres ID Sequences broken by recreate-table", - "Remove invalid labels from comments", - "Delete orphaned IssueLabels", - "Add LFS columns to Mirror", - "Convert avatar url to text", - "Delete credentials from past migrations", - "Always save primary email on email address table", - "Add issue resource index table", - "Create PushMirror table", - "Rename Task errors to message", - "Add new table repo_archiver", - "Create protected tag table", - "Drop unneeded webhook related columns", - "Add key is verified to gpg key", - "Unwrap ldap.Sources", - "Add agit flow pull request support", - "Alter issue/comment table TEXT fields to LONGTEXT", - "RecreateIssueResourceIndexTable to have a primary key instead of an unique index", - "Add repo id column for attachment table", - "Add Branch Protection Unprotected Files Column", - "Add table commit_status_index", - "Add Color to ProjectBoard table", - "Add renamed_branch table", - "Add issue content history table", - "No-op (remote version is using AppState now)", - "Add table app_state", - "Drop table remote_version (if exists)", - "Create key/value table for user settings", - "Add Sorting to ProjectIssue table", - "Add key is verified to ssh key", - "Migrate to higher varchar on user struct", - "Add authorize column to team_unit table", - "Add webauthn table and migrate u2f data to webauthn - NO-OPED", - "Use base32.HexEncoding instead of base64 encoding for cred ID as it is case insensitive - NO-OPED", - "Increase WebAuthentication CredentialID size to 410 - NO-OPED", - "v208 was completely broken - remigrate", - "Create ForeignReference table", - "Add package tables", - "Add allow edits from maintainers to PullRequest table", - "Add auto merge table", - "allow to view files in PRs", - "No-op (Improve Action table indices v1)", - "Alter hook_task table TEXT fields to LONGTEXT", - "Improve Action table indices v2", - "Add sync_on_commit column to push_mirror table", - "Add container repository property", - "Store WebAuthentication CredentialID as bytes and increase size to at least 1024", - "Drop old CredentialID column", - "Rename CredentialIDBytes column to CredentialID", - "Add badges to users", - "Alter gpg_key/public_key content TEXT fields to MEDIUMTEXT", - "Conan and generic packages do not need to be semantically versioned", - "Create key/value table for system settings", - "Add TeamInvite table", - "Update counts of all open milestones", - "Add ConfidentialClient column (default true) to OAuth2Application table", - "Add index for hook_task", - "Alter package_version.metadata_json to LONGTEXT", - "Add header_authorization_encrypted column to webhook table", - "Add package cleanup rule table", - "Add index for access_token", - "Create secrets table", - "Drop ForeignReference table", - "Add updated unix to LFSMetaObject", - "Add scope for access_token", - "Add actions tables", - "Add card_type column to project table", - "Alter gpg_key_import content TEXT field to MEDIUMTEXT", - "Add exclusive label", - "Add NeedApproval to actions tables", - "Rename Webhook org_id to owner_id", - "Add missed column owner_id for project table", - "Fix incorrect project type", - "Add version column to action_runner table", - "Improve Action table indices v3", - "Change Container Metadata", - "Fix incorrect owner team unit access mode", - "Fix incorrect admin team unit access mode", - "Fix ExternalTracker and ExternalWiki accessMode in owner and admin team", - "Add ActionTaskOutput table", - "Add ArchivedUnix Column", - "Add is_internal column to package", - "Add Actions Artifact table", - "Add PinOrder Column", - "Convert scoped access tokens", - "Drop custom_labels column of action_runner table", - "Add variable table", - "Add TriggerEvent to action_run table", - "Add git_size and lfs_size columns to repository table", - "Add branch table", - "Alter Actions Artifact table", - "Reduce commit status", - "Add action_tasks_version table", - "Update Action Ref", - "Drop deleted branch table", - "Fix PackageProperty typo", - "Allow archiving labels", - "Add Version to ActionRun table", - "Add Action Schedule Table", - "Add Actions artifacts expiration date", - "Add ScheduleID for ActionRun", - "Add RemoteAddress to mirrors", - "Add Index to issue_user.issue_id", - "Add Index to comment.dependent_issue_id", - "Add Index to action.user_id", - "Rename user themes", - "Add auth_token table", - "Add Index to pull_auto_merge.doer_id", - "Add combined Index to issue_user.uid and issue_id", - "Add ignore stale approval column on branch table", - "Add PreviousDuration to ActionRun", - "Add support for SHA256 git repositories", - "Use Slug instead of ID for Badges", - "Add user_blocking table", - "Add default_wiki_branch to repository table", - "Add PayloadVersion to HookTask", - "Add Index to attachment.comment_id", - "Ensure every project has exactly one default column - No Op", - "Ensure every project has exactly one default column", - "Add unique index for project issue table", - "Add commit status summary table", - "Add missing field of commit status summary table", - "Add everyone_access_mode for repo_unit", - "Drop wrongly created table o_auth2_application", - "Add content version to issue and comment table", - "Add force-push branch protection support", - "Add skip_secondary_authorization option to oauth2 application table", - "Add metadata column for comment table", - "Add index for release sha1", - "Add Repository Licenses", + Status uint8 `xorm:"SMALLINT NOT NULL"` + Source uint8 `xorm:"SMALLINT NOT NULL"` + + IssueID int64 `xorm:"NOT NULL"` + CommitID string + CommentID int64 + + UpdatedBy int64 `xorm:"NOT NULL"` + + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"` } -// Version describes the version table. Should have only one row with id==1 -type Version struct { - ID int64 `xorm:"pk autoincr"` - Version int64 +// TableName sets the name of this table +func (*improveNotificationTableIndicesAction) TableName() string { + return "notification" } -func MigrateToXormigrate(x *xorm.Engine) error { - if err := x.Sync(new(Version)); err != nil { - return fmt.Errorf("sync: %w", err) - } +// TableIndices implements xorm's TableIndices interface +func (*improveNotificationTableIndicesAction) TableIndices() []*schemas.Index { + indices := make([]*schemas.Index, 0, 8) + usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType) + usuuIndex.AddColumn("user_id", "status", "updated_unix") + indices = append(indices, usuuIndex) - currentVersion := &Version{ID: 1} - has, err := x.Get(currentVersion) - if err != nil { - return fmt.Errorf("get: %w", err) - } else if !has { - // This should not happen - return fmt.Errorf("could not get version") - } + // Add the individual indices that were previously defined in struct tags + userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType) + userIDIndex.AddColumn("user_id") + indices = append(indices, userIDIndex) - v := currentVersion.Version - if minDBVersion > v { - log.Fatal(`Gitea no longer supports auto-migration from your previously installed version. -Please try upgrading to i.e. v1.6.4 first, then upgrade to this version.`) - return nil - } + repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType) + repoIDIndex.AddColumn("repo_id") + indices = append(indices, repoIDIndex) - // Downgrading Gitea's database version not supported - if int(v-minDBVersion) > oldMigrationsCount { - msg := fmt.Sprintf("Your database (migration version: %d) is for a newer Gitea, you can not use the newer database for this old Gitea release (%d).", v, expectedVersion) - msg += "\nGitea will exit to keep your database safe and unchanged. Please use the correct Gitea release, do not change the migration version manually (incorrect manual operation may lose data)." - if !setting.IsProd { - msg += fmt.Sprintf("\nIf you are in development and really know what you're doing, you can force changing the migration version by executing: UPDATE version SET version=%d WHERE id=1;", expectedVersion) - } - log.Fatal("Migration Error: %s", msg) - return nil - } + statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType) + statusIndex.AddColumn("status") + indices = append(indices, statusIndex) - // add migrations that already have been run - for i := v; i < 307; i++ { - if _, err := x.Insert(&xormigrate.Migration{ID: fmt.Sprint(i)}); err != nil { - return err - } - } + sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType) + sourceIndex.AddColumn("source") + indices = append(indices, sourceIndex) - // Remove old version table - return x.DropTables(new(Version)) + issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType) + issueIDIndex.AddColumn("issue_id") + indices = append(indices, issueIDIndex) + + commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType) + commitIDIndex.AddColumn("commit_id") + indices = append(indices, commitIDIndex) + + updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType) + updatedByIndex.AddColumn("updated_by") + indices = append(indices, updatedByIndex) + + return indices +} + +func ImproveNotificationTableIndices(x *xorm.Engine) error { + return x.Sync(&improveNotificationTableIndicesAction{}) +>>>>>>> origin/main } diff --git a/models/migrations/v1_23/v310.go b/models/migrations/v1_23/v310.go new file mode 100644 index 0000000000..2752ca4862 --- /dev/null +++ b/models/migrations/v1_23/v310.go @@ -0,0 +1,311 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "fmt" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +const ( + minDBVersion = 70 // Gitea 1.5.3 + oldMigrationsCount = 230 + expectedVersion = minDBVersion + oldMigrationsCount +) + +var oldMigrationNames = []string{ + "add issue_dependencies", + "protect each scratch token", + "add review", + "add must_change_password column for users table", + "add approval whitelists to protected branches", + "clear nonused data which not deleted when user was deleted", + "add pull request rebase with merge commit", + "add theme to users", + "rename repo is_bare to repo is_empty", + "add can close issues via commit in any branch", + "add is locked to issues", + "update U2F counter type", + "hot fix for wrong release sha1 on release table", + "add uploader id for table attachment", + "add table to store original imported gpg keys", + "hash application token", + "add http method to webhook", + "add avatar field to repository", + "add commit status context field to commit_status", + "add original author/url migration info to issues, comments, and repo ", + "change length of some repository columns", + "add index on owner_id of repository and type, review_id of comment", + "remove orphaned repository index statuses", + "add email notification enabled preference to user", + "add enable_status_check, status_check_contexts to protected_branch", + "add table columns for cross referencing issues", + "delete orphaned attachments", + "add repo_admin_change_team_access to user", + "add original author name and id on migrated release", + "add task table and status column for repository table", + "update migration repositories' service type", + "change length of some external login users columns", + "update migration repositories' service type v2", + "Add WhitelistDeployKeys to protected branch", + "remove unnecessary columns from label", + "add includes_all_repositories to teams", + "add column `mode` to table watch", + "Add template options to repository", + "Add comment_id on table notification", + "add can_create_org_repo to team", + "change review content type to text", + "update branch protection for can push and whitelist enable", + "remove release attachments which repository deleted", + "new feature: change target branch of pull requests", + "Remove authentication credentials from stored URL", + "add user_id prefix to existing user avatar name", + "Extend TrackedTimes", + "Add block on rejected reviews branch protection", + "Add commit id and stale to reviews", + "Fix migrated repositories' git service type", + "Add owner_name on table repository", + "add is_restricted column for users table", + "Add Require Signed Commits to ProtectedBranch", + "Add original information for reactions", + "Add columns to user and repository", + "Add some columns on review for migration", + "Fix topic repository count", + "add repository code language statistics", + "fix merge base for pull requests", + "remove dependencies from deleted repositories", + "Expand webhooks for more granularity", + "Add IsSystemWebhook column to webhooks table", + "Add Branch Protection Protected Files Column", + "Add EmailHash Table", + "Refix merge base for merged pull requests", + "Add OrgID column to Labels table", + "Add CommitsAhead and CommitsBehind Column to PullRequest Table", + "Add Branch Protection Block Outdated Branch", + "Add ResolveDoerID to Comment table", + "prepend refs/heads/ to issue refs", + "Save detected language file size to database instead of percent", + "Add KeepActivityPrivate to User table", + "Ensure Repository.IsArchived is not null", + "recalculate Stars number for all user", + "update Matrix Webhook http method to 'PUT'", + "Increase Language field to 50 in LanguageStats", + "Add projects info to repository table", + "create review for 0 review id code comments", + "remove issue dependency comments who refer to non existing issues", + "Add Created and Updated to Milestone table", + "add primary key to repo_topic", + "set default password algorithm to Argon2", + "add TrustModel field to Repository", + "add Team review request support", + "add timestamps to Star, Label, Follow, Watch and Collaboration", + "add changed_protected_files column for pull_request table", + "fix publisher ID for tag releases", + "ensure repo topics are up-to-date", + "code comment replies should have the commitID of the review they are replying to", + "update reactions constraint", + "Add block on official review requests branch protection", + "Convert task type from int to string", + "Convert webhook task type from int to string", + "Convert topic name from 25 to 50", + "Add scope and nonce columns to oauth2_grant table", + "Convert hook task type from char(16) to varchar(16) and trim the column", + "Where Password is Valid with Empty String delete it", + "Add user redirect", + "Recreate user table to fix default values", + "Update DeleteBranch comments to set the old_ref to the commit_sha", + "Add Dismissed to Review table", + "Add Sorting to ProjectBoard table", + "Add sessions table for go-chi/session", + "Add time_id column to Comment", + "Create repo transfer table", + "Fix Postgres ID Sequences broken by recreate-table", + "Remove invalid labels from comments", + "Delete orphaned IssueLabels", + "Add LFS columns to Mirror", + "Convert avatar url to text", + "Delete credentials from past migrations", + "Always save primary email on email address table", + "Add issue resource index table", + "Create PushMirror table", + "Rename Task errors to message", + "Add new table repo_archiver", + "Create protected tag table", + "Drop unneeded webhook related columns", + "Add key is verified to gpg key", + "Unwrap ldap.Sources", + "Add agit flow pull request support", + "Alter issue/comment table TEXT fields to LONGTEXT", + "RecreateIssueResourceIndexTable to have a primary key instead of an unique index", + "Add repo id column for attachment table", + "Add Branch Protection Unprotected Files Column", + "Add table commit_status_index", + "Add Color to ProjectBoard table", + "Add renamed_branch table", + "Add issue content history table", + "No-op (remote version is using AppState now)", + "Add table app_state", + "Drop table remote_version (if exists)", + "Create key/value table for user settings", + "Add Sorting to ProjectIssue table", + "Add key is verified to ssh key", + "Migrate to higher varchar on user struct", + "Add authorize column to team_unit table", + "Add webauthn table and migrate u2f data to webauthn - NO-OPED", + "Use base32.HexEncoding instead of base64 encoding for cred ID as it is case insensitive - NO-OPED", + "Increase WebAuthentication CredentialID size to 410 - NO-OPED", + "v208 was completely broken - remigrate", + "Create ForeignReference table", + "Add package tables", + "Add allow edits from maintainers to PullRequest table", + "Add auto merge table", + "allow to view files in PRs", + "No-op (Improve Action table indices v1)", + "Alter hook_task table TEXT fields to LONGTEXT", + "Improve Action table indices v2", + "Add sync_on_commit column to push_mirror table", + "Add container repository property", + "Store WebAuthentication CredentialID as bytes and increase size to at least 1024", + "Drop old CredentialID column", + "Rename CredentialIDBytes column to CredentialID", + "Add badges to users", + "Alter gpg_key/public_key content TEXT fields to MEDIUMTEXT", + "Conan and generic packages do not need to be semantically versioned", + "Create key/value table for system settings", + "Add TeamInvite table", + "Update counts of all open milestones", + "Add ConfidentialClient column (default true) to OAuth2Application table", + "Add index for hook_task", + "Alter package_version.metadata_json to LONGTEXT", + "Add header_authorization_encrypted column to webhook table", + "Add package cleanup rule table", + "Add index for access_token", + "Create secrets table", + "Drop ForeignReference table", + "Add updated unix to LFSMetaObject", + "Add scope for access_token", + "Add actions tables", + "Add card_type column to project table", + "Alter gpg_key_import content TEXT field to MEDIUMTEXT", + "Add exclusive label", + "Add NeedApproval to actions tables", + "Rename Webhook org_id to owner_id", + "Add missed column owner_id for project table", + "Fix incorrect project type", + "Add version column to action_runner table", + "Improve Action table indices v3", + "Change Container Metadata", + "Fix incorrect owner team unit access mode", + "Fix incorrect admin team unit access mode", + "Fix ExternalTracker and ExternalWiki accessMode in owner and admin team", + "Add ActionTaskOutput table", + "Add ArchivedUnix Column", + "Add is_internal column to package", + "Add Actions Artifact table", + "Add PinOrder Column", + "Convert scoped access tokens", + "Drop custom_labels column of action_runner table", + "Add variable table", + "Add TriggerEvent to action_run table", + "Add git_size and lfs_size columns to repository table", + "Add branch table", + "Alter Actions Artifact table", + "Reduce commit status", + "Add action_tasks_version table", + "Update Action Ref", + "Drop deleted branch table", + "Fix PackageProperty typo", + "Allow archiving labels", + "Add Version to ActionRun table", + "Add Action Schedule Table", + "Add Actions artifacts expiration date", + "Add ScheduleID for ActionRun", + "Add RemoteAddress to mirrors", + "Add Index to issue_user.issue_id", + "Add Index to comment.dependent_issue_id", + "Add Index to action.user_id", + "Rename user themes", + "Add auth_token table", + "Add Index to pull_auto_merge.doer_id", + "Add combined Index to issue_user.uid and issue_id", + "Add ignore stale approval column on branch table", + "Add PreviousDuration to ActionRun", + "Add support for SHA256 git repositories", + "Use Slug instead of ID for Badges", + "Add user_blocking table", + "Add default_wiki_branch to repository table", + "Add PayloadVersion to HookTask", + "Add Index to attachment.comment_id", + "Ensure every project has exactly one default column - No Op", + "Ensure every project has exactly one default column", + "Add unique index for project issue table", + "Add commit status summary table", + "Add missing field of commit status summary table", + "Add everyone_access_mode for repo_unit", + "Drop wrongly created table o_auth2_application", + "Add content version to issue and comment table", + "Add force-push branch protection support", + "Add skip_secondary_authorization option to oauth2 application table", + "Add metadata column for comment table", + "Add index for release sha1", + "Add Repository Licenses", + "Improve Notification table indices", + "Add BlockAdminMergeOverride to ProtectedBranch", + "Fix milestone deadline_unix when there is no due date", + "Add index(user_id, is_deleted) for action table", +} + +// Version describes the version table. Should have only one row with id==1 +type Version struct { + ID int64 `xorm:"pk autoincr"` + Version int64 +} + +func MigrateToXormigrate(x *xorm.Engine) error { + if err := x.Sync(new(Version)); err != nil { + return fmt.Errorf("sync: %w", err) + } + + currentVersion := &Version{ID: 1} + has, err := x.Get(currentVersion) + if err != nil { + return fmt.Errorf("get: %w", err) + } else if !has { + // This should not happen + return fmt.Errorf("could not get version") + } + + v := currentVersion.Version + if minDBVersion > v { + log.Fatal(`Gitea no longer supports auto-migration from your previously installed version. +Please try upgrading to i.e. v1.6.4 first, then upgrade to this version.`) + return nil + } + + // Downgrading Gitea's database version not supported + if int(v-minDBVersion) > oldMigrationsCount { + msg := fmt.Sprintf("Your database (migration version: %d) is for a newer Gitea, you can not use the newer database for this old Gitea release (%d).", v, expectedVersion) + msg += "\nGitea will exit to keep your database safe and unchanged. Please use the correct Gitea release, do not change the migration version manually (incorrect manual operation may lose data)." + if !setting.IsProd { + msg += fmt.Sprintf("\nIf you are in development and really know what you're doing, you can force changing the migration version by executing: UPDATE version SET version=%d WHERE id=1;", expectedVersion) + } + log.Fatal("Migration Error: %s", msg) + return nil + } + + // add migrations that already have been run + for i := v; i < 307; i++ { + if _, err := x.Insert(&xormigrate.Migration{ID: fmt.Sprint(i)}); err != nil { + return err + } + } + + // Remove old version table + return x.DropTables(new(Version)) +} diff --git a/models/organization/mini_org.go b/models/organization/mini_org.go deleted file mode 100644 index b1b24624c5..0000000000 --- a/models/organization/mini_org.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package organization - -import ( - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" - user_model "code.gitea.io/gitea/models/user" - - "xorm.io/builder" -) - -// MinimalOrg represents a simple organization with only the needed columns -type MinimalOrg = Organization - -// GetUserOrgsList returns all organizations the given user has access to -func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) { - schema, err := db.TableInfo(new(user_model.User)) - if err != nil { - return nil, err - } - - outputCols := []string{ - "id", - "name", - "full_name", - "visibility", - "avatar", - "avatar_email", - "use_custom_avatar", - } - - groupByCols := &strings.Builder{} - for _, col := range outputCols { - fmt.Fprintf(groupByCols, "`%s`.%s,", schema.Name, col) - } - groupByStr := groupByCols.String() - groupByStr = groupByStr[0 : len(groupByStr)-1] - - sess := db.GetEngine(ctx) - sess = sess.Select(groupByStr+", count(distinct repo_id) as org_count"). - Table("user"). - Join("INNER", "team", "`team`.org_id = `user`.id"). - Join("INNER", "team_user", "`team`.id = `team_user`.team_id"). - Join("LEFT", builder. - Select("id as repo_id, owner_id as repo_owner_id"). - From("repository"). - Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)), "`repository`.repo_owner_id = `team`.org_id"). - Where("`team_user`.uid = ?", user.ID). - GroupBy(groupByStr) - - type OrgCount struct { - Organization `xorm:"extends"` - OrgCount int - } - - orgCounts := make([]*OrgCount, 0, 10) - - if err := sess. - Asc("`user`.name"). - Find(&orgCounts); err != nil { - return nil, err - } - - orgs := make([]*MinimalOrg, len(orgCounts)) - for i, orgCount := range orgCounts { - orgCount.Organization.NumRepos = orgCount.OrgCount - orgs[i] = &orgCount.Organization - } - - return orgs, nil -} diff --git a/models/organization/org.go b/models/organization/org.go index 6231f1eeed..725a99356e 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -25,13 +25,6 @@ import ( "xorm.io/xorm" ) -// ________ .__ __ .__ -// \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____ -// / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \ -// / | \ | \/ /_/ > __ \| | \ |/ / / __ \| | | ( <_> ) | \ -// \_______ /__| \___ (____ /___| /__/_____ \(____ /__| |__|\____/|___| / -// \/ /_____/ \/ \/ \/ \/ \/ - // ErrOrgNotExist represents a "OrgNotExist" kind of error. type ErrOrgNotExist struct { ID int64 @@ -465,42 +458,6 @@ func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*u And("team_user.org_id = ?", orgID).Find(&users) } -// SearchOrganizationsOptions options to filter organizations -type SearchOrganizationsOptions struct { - db.ListOptions - All bool -} - -// FindOrgOptions finds orgs options -type FindOrgOptions struct { - db.ListOptions - UserID int64 - IncludePrivate bool -} - -func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { - cond := builder.Eq{"uid": userID} - if !includePrivate { - cond["is_public"] = true - } - return builder.Select("org_id").From("org_user").Where(cond) -} - -func (opts FindOrgOptions) ToConds() builder.Cond { - var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization} - if opts.UserID > 0 { - cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate))) - } - if !opts.IncludePrivate { - cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}) - } - return cond -} - -func (opts FindOrgOptions) ToOrders() string { - return "`user`.name ASC" -} - // HasOrgOrUserVisible tells if the given user can see the given org or user func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool { // If user is nil, it's an anonymous user/request. @@ -533,20 +490,6 @@ func HasOrgsVisible(ctx context.Context, orgs []*Organization, user *user_model. return false } -// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID -// are allowed to create repos. -func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) { - orgs := make([]*Organization, 0, 10) - - return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`"). - Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id"). - Join("INNER", "`team`", "`team`.id = `team_user`.team_id"). - Where(builder.Eq{"`team_user`.uid": userID}). - And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))). - Asc("`user`.name"). - Find(&orgs) -} - // GetOrgUsersByOrgID returns all organization-user relations by organization ID. func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) { sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID) diff --git a/models/organization/org_list.go b/models/organization/org_list.go new file mode 100644 index 0000000000..72ebf6f178 --- /dev/null +++ b/models/organization/org_list.go @@ -0,0 +1,138 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package organization + +import ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" + + "xorm.io/builder" +) + +// SearchOrganizationsOptions options to filter organizations +type SearchOrganizationsOptions struct { + db.ListOptions + All bool +} + +// FindOrgOptions finds orgs options +type FindOrgOptions struct { + db.ListOptions + UserID int64 + IncludePrivate bool +} + +func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { + cond := builder.Eq{"uid": userID} + if !includePrivate { + cond["is_public"] = true + } + return builder.Select("org_id").From("org_user").Where(cond) +} + +func (opts FindOrgOptions) ToConds() builder.Cond { + var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization} + if opts.UserID > 0 { + cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate))) + } + if !opts.IncludePrivate { + cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}) + } + return cond +} + +func (opts FindOrgOptions) ToOrders() string { + return "`user`.lower_name ASC" +} + +// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID +// are allowed to create repos. +func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) { + orgs := make([]*Organization, 0, 10) + + return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`"). + Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id"). + Join("INNER", "`team`", "`team`.id = `team_user`.team_id"). + Where(builder.Eq{"`team_user`.uid": userID}). + And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))). + Asc("`user`.name"). + Find(&orgs) +} + +// MinimalOrg represents a simple organization with only the needed columns +type MinimalOrg = Organization + +// GetUserOrgsList returns all organizations the given user has access to +func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) { + schema, err := db.TableInfo(new(user_model.User)) + if err != nil { + return nil, err + } + + outputCols := []string{ + "id", + "name", + "full_name", + "visibility", + "avatar", + "avatar_email", + "use_custom_avatar", + } + + selectColumns := &strings.Builder{} + for i, col := range outputCols { + fmt.Fprintf(selectColumns, "`%s`.%s", schema.Name, col) + if i < len(outputCols)-1 { + selectColumns.WriteString(", ") + } + } + columnsStr := selectColumns.String() + + var orgs []*MinimalOrg + if err := db.GetEngine(ctx).Select(columnsStr). + Table("user"). + Where(builder.In("`user`.`id`", queryUserOrgIDs(user.ID, true))). + Find(&orgs); err != nil { + return nil, err + } + + type orgCount struct { + OrgID int64 + RepoCount int + } + var orgCounts []orgCount + if err := db.GetEngine(ctx). + Select("owner_id AS org_id, COUNT(DISTINCT(repository.id)) as repo_count"). + Table("repository"). + Join("INNER", "org_user", "owner_id = org_user.org_id"). + Where("org_user.uid = ?", user.ID). + And(builder.Or( + builder.Eq{"repository.is_private": false}, + builder.In("repository.id", builder.Select("repo_id").From("team_repo"). + InnerJoin("team_user", "team_user.team_id = team_repo.team_id"). + Where(builder.Eq{"team_user.uid": user.ID})), + builder.In("repository.id", builder.Select("repo_id").From("collaboration"). + Where(builder.Eq{"user_id": user.ID})), + )). + GroupBy("owner_id").Find(&orgCounts); err != nil { + return nil, err + } + + orgCountMap := make(map[int64]int, len(orgCounts)) + for _, orgCount := range orgCounts { + orgCountMap[orgCount.OrgID] = orgCount.RepoCount + } + + for _, org := range orgs { + org.NumRepos = orgCountMap[org.ID] + } + + return orgs, nil +} diff --git a/models/organization/org_list_test.go b/models/organization/org_list_test.go new file mode 100644 index 0000000000..fc8d148a1d --- /dev/null +++ b/models/organization/org_list_test.go @@ -0,0 +1,62 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package organization_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestCountOrganizations(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{}) + assert.NoError(t, err) + cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true}) + assert.NoError(t, err) + assert.Equal(t, expected, cnt) +} + +func TestFindOrgs(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ + UserID: 4, + IncludePrivate: true, + }) + assert.NoError(t, err) + if assert.Len(t, orgs, 1) { + assert.EqualValues(t, 3, orgs[0].ID) + } + + orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ + UserID: 4, + IncludePrivate: false, + }) + assert.NoError(t, err) + assert.Len(t, orgs, 0) + + total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ + UserID: 4, + IncludePrivate: true, + }) + assert.NoError(t, err) + assert.EqualValues(t, 1, total) +} + +func TestGetUserOrgsList(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4}) + assert.NoError(t, err) + if assert.Len(t, orgs, 1) { + assert.EqualValues(t, 3, orgs[0].ID) + // repo_id: 3 is in the team, 32 is public, 5 is private with no team + assert.EqualValues(t, 2, orgs[0].NumRepos) + } +} diff --git a/models/organization/org_test.go b/models/organization/org_test.go index c614aaacf5..7159f0fc46 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -129,15 +129,6 @@ func TestGetOrgByName(t *testing.T) { assert.True(t, organization.IsErrOrgNotExist(err)) } -func TestCountOrganizations(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{}) - assert.NoError(t, err) - cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true}) - assert.NoError(t, err) - assert.Equal(t, expected, cnt) -} - func TestIsOrganizationOwner(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(orgID, userID int64, expected bool) { @@ -251,33 +242,6 @@ func TestRestrictedUserOrgMembers(t *testing.T) { } } -func TestFindOrgs(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: 4, - IncludePrivate: true, - }) - assert.NoError(t, err) - if assert.Len(t, orgs, 1) { - assert.EqualValues(t, 3, orgs[0].ID) - } - - orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: 4, - IncludePrivate: false, - }) - assert.NoError(t, err) - assert.Len(t, orgs, 0) - - total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: 4, - IncludePrivate: true, - }) - assert.NoError(t, err) - assert.EqualValues(t, 1, total) -} - func TestGetOrgUsersByOrgID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/project/project.go b/models/project/project.go index 050ccf44e0..9779908b9d 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -242,6 +242,7 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy { } // NewProject creates a new Project +// The title will be cut off at 255 characters if it's longer than 255 characters. func NewProject(ctx context.Context, p *Project) error { if !IsTemplateTypeValid(p.TemplateType) { p.TemplateType = TemplateTypeNone @@ -255,6 +256,8 @@ func NewProject(ctx context.Context, p *Project) error { return util.NewInvalidArgumentErrorf("project type is not valid") } + p.Title, _ = util.SplitStringAtByteN(p.Title, 255) + return db.WithTx(ctx, func(ctx context.Context) error { if err := db.Insert(ctx, p); err != nil { return err @@ -308,6 +311,7 @@ func UpdateProject(ctx context.Context, p *Project) error { p.CardType = CardTypeTextOnly } + p.Title, _ = util.SplitStringAtByteN(p.Title, 255) _, err := db.GetEngine(ctx).ID(p.ID).Cols( "title", "description", diff --git a/models/repo/release.go b/models/repo/release.go index 7c66cbc1af..ba7a3b3159 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -156,6 +156,7 @@ func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, er // UpdateRelease updates all columns of a release func UpdateRelease(ctx context.Context, rel *Release) error { + rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255) _, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel) return err } diff --git a/models/repo/repo.go b/models/repo/repo.go index 68f8e16a21..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,13 +474,11 @@ 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, - "mode": "comment", } unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker) @@ -509,22 +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 - } - metas["mode"] = "document" - 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_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/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/console/console.go b/modules/markup/console/console.go index 01653565fe..d991527b80 100644 --- a/modules/markup/console/console.go +++ b/modules/markup/console/console.go @@ -8,7 +8,6 @@ import ( "io" "path/filepath" "regexp" - "strings" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" @@ -17,9 +16,6 @@ import ( "github.com/go-enry/go-enry/v2" ) -// MarkupName describes markup's name -var MarkupName = "console" - func init() { markup.RegisterRenderer(Renderer{}) } @@ -29,7 +25,7 @@ type Renderer struct{} // Name implements markup.Renderer func (Renderer) Name() string { - return MarkupName + return "console" } // Extensions implements markup.Renderer @@ -67,20 +63,3 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri _, err = output.Write(buf) return err } - -// Render renders terminal colors to HTML with all specific handling stuff. -func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { - if ctx.Type == "" { - ctx.Type = MarkupName - } - return markup.Render(ctx, input, output) -} - -// RenderString renders terminal colors in string to HTML with all specific handling stuff and return string -func RenderString(ctx *markup.RenderContext, content string) (string, error) { - var buf strings.Builder - if err := Render(ctx, strings.NewReader(content), &buf); err != nil { - return "", err - } - return buf.String(), nil -} diff --git a/modules/markup/html.go b/modules/markup/html.go index e2eefefc4b..16ccd4b406 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,27 @@ const ( IssueNameStyleRegexp = "regexp" ) -var ( +// CSS class for action keywords (e.g. "closes: #1") +const keywordClass = "issue-keyword" + +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 + + 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 +56,56 @@ 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`) + + 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") @@ -197,13 +194,6 @@ func RenderCommitMessage( 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) } @@ -231,16 +221,17 @@ var emojiProcessors = []processor{ // which changes every text node into a link to the passed default link. func RenderCommitMessageSubject( ctx *RenderContext, - content string, + defaultLink, 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)) - } + 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) } @@ -249,10 +240,8 @@ 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) @@ -288,11 +277,6 @@ func RenderEmoji( 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 +290,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(""), )) @@ -353,7 +337,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod // 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 @@ -442,12 +426,11 @@ func createLink(href, content, class string) *html.Node { a := &html.Node{ Type: html.ElementNode, Data: atom.A.String(), - Attr: []html.Attribute{ - {Key: "href", Val: href}, - {Key: "data-markdown-generated-content"}, - }, + Attr: []html.Attribute{{Key: "href", Val: href}}, + } + if !RenderBehaviorForTesting.DisableInternalAttributes { + 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}) } diff --git a/modules/markup/html_codepreview_test.go b/modules/markup/html_codepreview_test.go index a90de278f5..5054627dde 100644 --- a/modules/markup/html_codepreview_test.go +++ b/modules/markup/html_codepreview_test.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" "github.com/stretchr/testify/assert" ) @@ -23,8 +24,8 @@ func TestRenderCodePreview(t *testing.T) { }) test := func(input, expected string) { buffer, err := markup.RenderString(&markup.RenderContext{ - Ctx: git.DefaultContext, - Type: "markdown", + Ctx: git.DefaultContext, + MarkupType: markdown.MarkupName, }, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) 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..32d0285eb4 100644 --- a/modules/markup/html_email.go +++ b/modules/markup/html_email.go @@ -9,7 +9,7 @@ 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 } diff --git a/modules/markup/html_emoji.go b/modules/markup/html_emoji.go index c60d06b823..6eacb2067f 100644 --- a/modules/markup/html_emoji.go +++ b/modules/markup/html_emoji.go @@ -62,7 +62,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 } diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 8f516751b0..cdcc94d563 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + testModule "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -39,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{ @@ -61,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) { @@ -259,14 +269,13 @@ 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}", "user": "someUser", "repo": "someRepo", "style": IssueNameStyleNumeric, - "mode": "document", } testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{ @@ -283,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 == "" { @@ -316,8 +341,7 @@ func TestRender_AutoLink(t *testing.T) { Links: Links{ Base: TestRepoURL, }, - Metas: localMetas, - IsWiki: true, + Metas: localWikiMetas, }, strings.NewReader(input), &buffer) assert.Equal(t, err, nil) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) @@ -340,7 +364,7 @@ func TestRender_AutoLink(t *testing.T) { func TestRender_FullIssueURLs(t *testing.T) { setting.AppURL = TestAppURL - + defer testModule.MockVariableValue(&RenderBehaviorForTesting.DisableInternalAttributes, true)() test := func(input, expected string) { var result strings.Builder err := postProcess(&RenderContext{ @@ -351,9 +375,7 @@ func TestRender_FullIssueURLs(t *testing.T) { Metas: localMetas, }, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result) assert.NoError(t, err) - actual := result.String() - actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") - assert.Equal(t, expected, actual) + assert.Equal(t, expected, result.String()) } test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6", "Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6") @@ -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 b6d4ed6a8e..2acf154ad2 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) @@ -67,9 +71,10 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { return } - // FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered? - // The "mode" approach should be refactored to some other more clear&reliable way. - crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki + // 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 diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go index 9350634568..b7562d0aa6 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.IsWiki { + 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.IsWiki), link) + link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link) } title := props["title"] if title == "" { @@ -200,25 +200,6 @@ func linkProcessor(ctx *RenderContext, node *html.Node) { } } -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 diff --git a/modules/markup/html_node.go b/modules/markup/html_node.go index 6d784975b9..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.IsWiki), 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.IsWiki), 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 82aded4407..67ac2758a3 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" + testModule "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -26,6 +27,11 @@ var ( "user": testRepoOwnerName, "repo": testRepoName, } + localWikiMetas = map[string]string{ + "user": testRepoOwnerName, + "repo": testRepoName, + "markupContentMode": "wiki", + } ) type mockRepo struct { @@ -104,7 +110,7 @@ func TestRender_Commits(t *testing.T) { func TestRender_CrossReferences(t *testing.T) { setting.AppURL = markup.TestAppURL - + defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() test := func(input, expected string) { buffer, err := markup.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, @@ -116,9 +122,7 @@ func TestRender_CrossReferences(t *testing.T) { Metas: localMetas, }, input) assert.NoError(t, err) - actual := strings.TrimSpace(buffer) - actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") - assert.Equal(t, strings.TrimSpace(expected), actual) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } test( @@ -148,7 +152,7 @@ func TestRender_CrossReferences(t *testing.T) { func TestRender_links(t *testing.T) { setting.AppURL = markup.TestAppURL - + defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() test := func(input, expected string) { buffer, err := markup.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, @@ -158,9 +162,7 @@ func TestRender_links(t *testing.T) { }, }, input) assert.NoError(t, err) - actual := strings.TrimSpace(buffer) - actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") - assert.Equal(t, strings.TrimSpace(expected), actual) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } oldCustomURLSchemes := setting.Markdown.CustomURLSchemes @@ -261,7 +263,7 @@ func TestRender_links(t *testing.T) { func TestRender_email(t *testing.T) { setting.AppURL = markup.TestAppURL - + defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() test := func(input, expected string) { res, err := markup.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, @@ -271,9 +273,7 @@ func TestRender_email(t *testing.T) { }, }, input) assert.NoError(t, err) - actual := strings.TrimSpace(res) - actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") - assert.Equal(t, strings.TrimSpace(expected), actual) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) } // Text that should be turned into email link @@ -302,10 +302,10 @@ func TestRender_email(t *testing.T) { j.doe@example.com; j.doe@example.com? j.doe@example.com!`, - `j.doe@example.com,
-j.doe@example.com.
-j.doe@example.com;
-j.doe@example.com?
+ `
j.doe@example.com, +j.doe@example.com. +j.doe@example.com; +j.doe@example.com? j.doe@example.com!
`) // Test that should *not* be turned into email links @@ -418,8 +418,7 @@ func TestRender_ShortLinks(t *testing.T) { Links: markup.Links{ Base: markup.TestRepoURL, }, - Metas: localMetas, - IsWiki: true, + Metas: localWikiMetas, }, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) @@ -531,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, - IsWiki: isWiki, + Ctx: git.DefaultContext, + Links: links, + Metas: util.Iif(isWiki, localWikiMetas, localMetas), }, input) assert.NoError(t, err) return strings.TrimSpace(string(buffer)) @@ -604,12 +602,7 @@ func Test_ParseClusterFuzz(t *testing.T) { func TestPostProcess_RenderDocument(t *testing.T) { setting.AppURL = markup.TestAppURL setting.StaticURLPrefix = markup.TestAppURL // can't run standalone - - localMetas := map[string]string{ - "user": "go-gitea", - "repo": "gitea", - "mode": "document", - } + defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() test := func(input, expected string) { var res strings.Builder @@ -619,12 +612,10 @@ func TestPostProcess_RenderDocument(t *testing.T) { AbsolutePrefix: true, Base: "https://example.com", }, - Metas: localMetas, + Metas: map[string]string{"user": "go-gitea", "repo": "gitea"}, }, strings.NewReader(input), &res) assert.NoError(t, err) - actual := strings.TrimSpace(res.String()) - actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "") - assert.Equal(t, strings.TrimSpace(expected), actual) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String())) } // Issue index shouldn't be post processing in a document. diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 0cd9dc5f30..c837b21e78 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -72,9 +72,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa g.transformList(ctx, v, rc) case *ast.Text: if v.SoftLineBreak() && !v.HardLineBreak() { - if ctx.Metas["mode"] != "document" { + // TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }` + // many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting + // especially in many tests. + markdownLineBreakStyle := ctx.Metas["markdownLineBreakStyle"] + if markup.RenderBehaviorForTesting.ForceHardLineBreak { + v.SetHardLineBreak(true) + } else if markdownLineBreakStyle == "comment" { v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) - } else { + } else if markdownLineBreakStyle == "document" { v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) } } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index db4e5706f6..6af0deb27b 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -257,9 +257,7 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri // Render renders Markdown to HTML with all specific handling stuff. func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { - if ctx.Type == "" { - ctx.Type = MarkupName - } + ctx.MarkupType = MarkupName return markup.Render(ctx, input, output) } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index ad38e7a088..780df8727f 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -36,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 @@ -74,7 +81,7 @@ func TestRender_StandardLinks(t *testing.T) { Links: markup.Links{ Base: FullURL, }, - IsWiki: true, + Metas: localWikiMetas, }, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) @@ -296,23 +303,21 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno } func TestTotal_RenderWiki(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)() + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() setting.AppURL = AppURL - answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw")) - for i := 0; i < len(sameCases); i++ { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ Base: FullURL, }, - Repo: newMockRepo(testRepoOwnerName, testRepoName), - Metas: localMetas, - IsWiki: true, + Repo: newMockRepo(testRepoOwnerName, testRepoName), + Metas: localWikiMetas, }, sameCases[i]) assert.NoError(t, err) - actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "") - assert.Equal(t, answers[i], actual) + assert.Equal(t, answers[i], string(line)) } testCases := []string{ @@ -334,19 +339,18 @@ func TestTotal_RenderWiki(t *testing.T) { Links: markup.Links{ Base: FullURL, }, - IsWiki: true, + Metas: localWikiMetas, }, testCases[i]) assert.NoError(t, err) - actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "") - assert.EqualValues(t, testCases[i+1], actual) + assert.EqualValues(t, testCases[i+1], string(line)) } } func TestTotal_RenderString(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)() + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)() setting.AppURL = AppURL - answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master")) - for i := 0; i < len(sameCases); i++ { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, @@ -358,8 +362,7 @@ func TestTotal_RenderString(t *testing.T) { Metas: localMetas, }, sameCases[i]) assert.NoError(t, err) - actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "") - assert.Equal(t, answers[i], actual) + assert.Equal(t, answers[i], string(line)) } testCases := []string{} @@ -428,6 +431,7 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) { expected := ` ` + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)() res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase) assert.NoError(t, err) assert.Equal(t, expected, res) @@ -658,9 +662,9 @@ mail@domain.com88fc37a3c0...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