diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 081e4a2db4..88ae98db8d 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -642,7 +642,7 @@ rules:
no-this-before-super: [2]
no-throw-literal: [2]
no-undef-init: [2]
- no-undef: [2, {typeof: true}]
+ no-undef: [2, {typeof: true}] # TODO: disable this rule after tsc passes
no-undefined: [0]
no-underscore-dangle: [0]
no-unexpected-multiline: [2]
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/cmd/serv.go b/cmd/serv.go
index 2d2df8aa23..d2271b68d2 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -111,12 +111,10 @@ func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error
if !setting.IsProd {
_, _ = fmt.Fprintln(os.Stderr, "Gitea:", logMsg)
}
- if userMessage != "" {
- if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) {
- logMsg = userMessage + " " + logMsg
- } else {
- logMsg = userMessage + ". " + logMsg
- }
+ if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) {
+ logMsg = userMessage + " " + logMsg
+ } else {
+ logMsg = userMessage + ". " + logMsg
}
_ = private.SSHLog(ctx, true, logMsg)
}
@@ -288,10 +286,10 @@ func runServ(c *cli.Context) error {
if allowedCommands.Contains(verb) {
if allowedCommandsLfs.Contains(verb) {
if !setting.LFS.StartServer {
- return fail(ctx, "Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled")
+ return fail(ctx, "LFS Server is not enabled", "")
}
if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH {
- return fail(ctx, "Unknown git command", "LFS SSH transfer connection denied, pure SSH protocol is disabled")
+ return fail(ctx, "LFS SSH transfer is not enabled", "")
}
if len(words) > 2 {
lfsVerb = words[2]
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index f201ff1d19..ef5684237d 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1007,6 +1007,14 @@ LEVEL = Info
;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
;DEFAULT_FORK_REPO_UNITS = repo.code,repo.pulls
;;
+;; Comma separated list of default mirror repo units.
+;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
+;DEFAULT_MIRROR_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.wiki,repo.projects,repo.packages
+;;
+;; Comma separated list of default template repo units.
+;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
+;DEFAULT_TEMPLATE_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.pulls,repo.wiki,repo.projects,repo.packages
+;;
;; Prefix archive files by placing them in a directory named after the repository
;PREFIX_ARCHIVE_FILES = true
;;
@@ -1904,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..e3655b627e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -22,14 +22,13 @@
gzip
# frontend
- nodejs_20
+ nodejs_22
# linting
python312
poetry
# backend
- go_1_22
gofumpt
sqlite
];
diff --git a/go.mod b/go.mod
index ff0d612133..bbd8186868 100644
--- a/go.mod
+++ b/go.mod
@@ -88,7 +88,7 @@ require (
github.com/markbates/goth v1.80.0
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-sqlite3 v1.14.24
- github.com/meilisearch/meilisearch-go v0.29.0
+ github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a
github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.27
github.com/microsoft/go-mssqldb v1.7.2
@@ -222,7 +222,7 @@ require (
github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-webauthn/x v0.1.15 // indirect
github.com/goccy/go-json v0.10.3 // indirect
- github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
@@ -330,6 +330,7 @@ replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142
replace github.com/nektos/act => gitea.com/gitea/act v0.261.3
+// TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why
replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0
// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
diff --git a/go.sum b/go.sum
index 0d2e4f1321..df3b7d899c 100644
--- a/go.sum
+++ b/go.sum
@@ -98,7 +98,6 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1L
github.com/anchore/archiver/v3 v3.5.2 h1:Bjemm2NzuRhmHy3m0lRe5tNoClB9A4zYyDV58PaB6aA=
github.com/anchore/archiver/v3 v3.5.2/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
-github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
@@ -391,8 +390,8 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7w
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
-github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
-github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
+github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
@@ -603,8 +602,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/meilisearch/meilisearch-go v0.29.0 h1:HZ9NEKN59USINQ/DXJge/aaXq8IrsKbXGTdAoBaaDz4=
-github.com/meilisearch/meilisearch-go v0.29.0/go.mod h1:2cRCAn4ddySUsFfNDLVPod/plRibQsJkXF/4gLhxbOk=
+github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a h1:F0y+3QtCG00mr4KueQWuHv1tlIQeNXhH+XAKYLhb3X4=
+github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a/go.mod h1:NYOgjEGt/+oExD+NixreBMqxtIB0kCndXOOgpGhoqEs=
github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
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/actions/task.go b/models/actions/task.go
index b62a0c351b..af74faf937 100644
--- a/models/actions/task.go
+++ b/models/actions/task.go
@@ -341,7 +341,7 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
// UpdateTaskByState updates the task by the state.
// It will always update the task if the state is not final, even there is no change.
// So it will update ActionTask.Updated to avoid the task being judged as a zombie task.
-func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionTask, error) {
+func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.TaskState) (*ActionTask, error) {
stepStates := map[int64]*runnerv1.StepState{}
for _, v := range state.Steps {
stepStates[v.Id] = v
@@ -360,6 +360,8 @@ func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionT
return nil, err
} else if !has {
return nil, util.ErrNotExist
+ } else if runnerID != task.RunnerID {
+ return nil, fmt.Errorf("invalid runner for task")
}
if task.Status.IsDone() {
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/db/collation.go b/models/db/collation.go
index c128cf5029..a7db9f5442 100644
--- a/models/db/collation.go
+++ b/models/db/collation.go
@@ -68,7 +68,8 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) {
var candidateCollations []string
if x.Dialect().URI().DBType == schemas.MYSQL {
- if _, err = x.SQL("SELECT @@collation_database").Get(&res.DatabaseCollation); err != nil {
+ _, err = x.SQL("SELECT DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?", setting.Database.Name).Get(&res.DatabaseCollation)
+ if err != nil {
return nil, err
}
res.IsCollationCaseSensitive = func(s string) bool {
diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml
index 443effe08c..d88a8ed8a9 100644
--- a/models/fixtures/action_task.yml
+++ b/models/fixtures/action_task.yml
@@ -1,3 +1,22 @@
+-
+ id: 46
+ attempt: 3
+ runner_id: 1
+ status: 3 # 3 is the status code for "cancelled"
+ started: 1683636528
+ stopped: 1683636626
+ repo_id: 4
+ owner_id: 1
+ commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+ is_fork_pull_request: 0
+ token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2aaaaa
+ token_salt: eeeeeeee
+ token_last_eight: eeeeeeee
+ log_filename: artifact-test2/2f/47.log
+ log_in_storage: 1
+ log_length: 707
+ log_size: 90179
+ log_expired: 0
-
id: 47
job_id: 192
diff --git a/models/fixtures/lfs_meta_object.yml b/models/fixtures/lfs_meta_object.yml
index 1c29e02d44..ae5ae56542 100644
--- a/models/fixtures/lfs_meta_object.yml
+++ b/models/fixtures/lfs_meta_object.yml
@@ -11,7 +11,7 @@
id: 2
oid: 2eccdb43825d2a49d99d542daa20075cff1d97d9d2349a8977efe9c03661737c
- size: 107
+ size: 2048
repository_id: 54
created_unix: 1671607299
diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml
index cf21b84aa9..73a3e9dba9 100644
--- a/models/fixtures/org_user.yml
+++ b/models/fixtures/org_user.yml
@@ -129,3 +129,9 @@
uid: 2
org_id: 35
is_public: true
+
+-
+ id: 23
+ uid: 20
+ org_id: 17
+ is_public: false
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/fixtures/system_setting.yml b/models/fixtures/system_setting.yml
index 30542bc82a..dcad176c89 100644
--- a/models/fixtures/system_setting.yml
+++ b/models/fixtures/system_setting.yml
@@ -1,7 +1,7 @@
-
id: 1
setting_key: 'picture.disable_gravatar'
- setting_value: 'false'
+ setting_value: 'true'
version: 1
created: 1653533198
updated: 1653533198
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index c0296deec5..b3bece5589 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -23,9 +23,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar1
+ avatar: ""
avatar_email: user1@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -60,8 +60,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar2
+ avatar: ""
avatar_email: user2@example.com
+ # cause a random avatar to be generated when referenced for test purposes
use_custom_avatar: false
num_followers: 2
num_following: 1
@@ -97,9 +98,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar3
+ avatar: ""
avatar_email: org3@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -134,9 +135,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar4
+ avatar: ""
avatar_email: user4@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 1
num_stars: 0
@@ -171,9 +172,9 @@
allow_import_local: false
allow_create_organization: false
prohibit_login: false
- avatar: avatar5
+ avatar: ""
avatar_email: user5@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -208,9 +209,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar6
+ avatar: ""
avatar_email: org6@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -245,9 +246,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar7
+ avatar: ""
avatar_email: org7@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -282,9 +283,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar8
+ avatar: ""
avatar_email: user8@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 1
num_following: 1
num_stars: 0
@@ -319,9 +320,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar9
+ avatar: ""
avatar_email: user9@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -332,6 +333,7 @@
repo_admin_change_team_access: false
theme: ""
keep_activity_private: false
+ created_unix: 1730468968
-
id: 10
@@ -356,9 +358,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar10
+ avatar: ""
avatar_email: user10@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 2
@@ -393,9 +395,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar11
+ avatar: ""
avatar_email: user11@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -430,9 +432,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar12
+ avatar: ""
avatar_email: user12@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -467,9 +469,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar13
+ avatar: ""
avatar_email: user13@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -504,9 +506,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar14
+ avatar: ""
avatar_email: user13@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -541,9 +543,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar15
+ avatar: ""
avatar_email: user15@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -578,9 +580,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar16
+ avatar: ""
avatar_email: user16@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -615,15 +617,15 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar17
+ avatar: ""
avatar_email: org17@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
num_repos: 2
num_teams: 3
- num_members: 4
+ num_members: 5
visibility: 0
repo_admin_change_team_access: false
theme: ""
@@ -652,9 +654,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar18
+ avatar: ""
avatar_email: user18@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -689,9 +691,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar19
+ avatar: ""
avatar_email: org19@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -726,9 +728,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar20
+ avatar: ""
avatar_email: user20@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -763,9 +765,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar21
+ avatar: ""
avatar_email: user21@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -800,9 +802,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar22
+ avatar: ""
avatar_email: limited_org@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -837,9 +839,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar23
+ avatar: ""
avatar_email: privated_org@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -874,9 +876,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar24
+ avatar: ""
avatar_email: user24@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -911,9 +913,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar25
+ avatar: ""
avatar_email: org25@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -948,9 +950,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar26
+ avatar: ""
avatar_email: org26@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -985,9 +987,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar27
+ avatar: ""
avatar_email: user27@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1022,9 +1024,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar28
+ avatar: ""
avatar_email: user28@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1059,9 +1061,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar29
+ avatar: ""
avatar_email: user29@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1096,9 +1098,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar29
+ avatar: ""
avatar_email: user30@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1133,9 +1135,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar31
+ avatar: ""
avatar_email: user31@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 1
num_stars: 0
@@ -1170,9 +1172,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar32
+ avatar: ""
avatar_email: user30@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1207,9 +1209,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar33
+ avatar: ""
avatar_email: user33@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 1
num_following: 0
num_stars: 0
@@ -1245,7 +1247,7 @@
allow_import_local: false
allow_create_organization: false
prohibit_login: false
- avatar: avatar34
+ avatar: ""
avatar_email: user34@example.com
use_custom_avatar: true
num_followers: 0
@@ -1282,9 +1284,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar35
+ avatar: ""
avatar_email: private_org35@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1319,9 +1321,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar22
+ avatar: ""
avatar_email: abcde@gitea.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1356,9 +1358,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: true
- avatar: avatar29
+ avatar: ""
avatar_email: user37@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1393,9 +1395,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar38
+ avatar: ""
avatar_email: user38@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1430,9 +1432,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar39
+ avatar: ""
avatar_email: user39@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1467,9 +1469,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar40
+ avatar: ""
avatar_email: user40@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1504,9 +1506,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar41
+ avatar: ""
avatar_email: org41@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
@@ -1541,9 +1543,9 @@
allow_import_local: false
allow_create_organization: true
prohibit_login: false
- avatar: avatar42
+ avatar: ""
avatar_email: org42@example.com
- use_custom_avatar: false
+ use_custom_avatar: true
num_followers: 0
num_following: 0
num_stars: 0
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 5969d21960..39fdd056d1 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -366,7 +366,8 @@ func prepareMigrationTasks() []*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, "Add DeleteBranchAfterMerge to AutoMerge", v1_23.AddDeleteBranchAfterMergeForAutoMerge),
+ newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices),
+ newMigration(310, "Add DeleteBranchAfterMerge to AutoMerge", v1_23.AddDeleteBranchAfterMergeForAutoMerge),
}
return preparedMigrations
}
diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go
index ed1bc3bda5..15177bf040 100644
--- a/models/migrations/v1_21/v276.go
+++ b/models/migrations/v1_21/v276.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"xorm.io/xorm"
)
@@ -163,7 +164,9 @@ func migratePushMirrors(x *xorm.Engine) error {
func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
-
+ if exist, _ := util.IsExist(repoPath); !exist {
+ return "", nil
+ }
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
if err != nil {
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
diff --git a/models/migrations/v1_23/v309.go b/models/migrations/v1_23/v309.go
index 6a2868ea4c..5b39398443 100644
--- a/models/migrations/v1_23/v309.go
+++ b/models/migrations/v1_23/v309.go
@@ -7,23 +7,71 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
+ "xorm.io/xorm/schemas"
)
-type pullAutoMerge struct {
- ID int64 `xorm:"pk autoincr"`
- PullID int64 `xorm:"UNIQUE"`
- DoerID int64 `xorm:"INDEX NOT NULL"`
- MergeStyle string `xorm:"varchar(30)"`
- Message string `xorm:"LONGTEXT"`
- DeleteBranchAfterMerge bool
- CreatedUnix timeutil.TimeStamp `xorm:"created"`
+type improveNotificationTableIndicesAction struct {
+ ID int64 `xorm:"pk autoincr"`
+ UserID int64 `xorm:"NOT NULL"`
+ RepoID int64 `xorm:"NOT NULL"`
+
+ 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"`
}
-// TableName return database table name for xorm
-func (pullAutoMerge) TableName() string {
- return "pull_auto_merge"
+// TableName sets the name of this table
+func (*improveNotificationTableIndicesAction) TableName() string {
+ return "notification"
}
-func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error {
- return x.Sync(new(pullAutoMerge))
+// 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)
+
+ // 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 ImproveNotificationTableIndices(x *xorm.Engine) error {
+ return x.Sync(&improveNotificationTableIndicesAction{})
}
diff --git a/models/migrations/v1_23/v310.go b/models/migrations/v1_23/v310.go
new file mode 100644
index 0000000000..6a2868ea4c
--- /dev/null
+++ b/models/migrations/v1_23/v310.go
@@ -0,0 +1,29 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/xorm"
+)
+
+type pullAutoMerge struct {
+ ID int64 `xorm:"pk autoincr"`
+ PullID int64 `xorm:"UNIQUE"`
+ DoerID int64 `xorm:"INDEX NOT NULL"`
+ MergeStyle string `xorm:"varchar(30)"`
+ Message string `xorm:"LONGTEXT"`
+ DeleteBranchAfterMerge bool
+ CreatedUnix timeutil.TimeStamp `xorm:"created"`
+}
+
+// TableName return database table name for xorm
+func (pullAutoMerge) TableName() string {
+ return "pull_auto_merge"
+}
+
+func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error {
+ return x.Sync(new(pullAutoMerge))
+}
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 b33d15d29c..725a99356e 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -22,15 +22,9 @@ import (
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
+ "xorm.io/xorm"
)
-// ________ .__ __ .__
-// \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____
-// / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \
-// / | \ | \/ /_/ > __ \| | \ |/ / / __ \| | | ( <_> ) | \
-// \_______ /__| \___ (____ /___| /__/_____ \(____ /__| |__|\____/|___| /
-// \/ /_____/ \/ \/ \/ \/ \/
-
// ErrOrgNotExist represents a "OrgNotExist" kind of error.
type ErrOrgNotExist struct {
ID int64
@@ -141,8 +135,9 @@ func (org *Organization) LoadTeams(ctx context.Context) ([]*Team, error) {
}
// GetMembers returns all members of organization.
-func (org *Organization) GetMembers(ctx context.Context) (user_model.UserList, map[int64]bool, error) {
+func (org *Organization) GetMembers(ctx context.Context, doer *user_model.User) (user_model.UserList, map[int64]bool, error) {
return FindOrgMembers(ctx, &FindOrgMembersOpts{
+ Doer: doer,
OrgID: org.ID,
})
}
@@ -195,16 +190,39 @@ func (org *Organization) CanCreateRepo() bool {
// FindOrgMembersOpts represensts find org members conditions
type FindOrgMembersOpts struct {
db.ListOptions
- OrgID int64
- PublicOnly bool
+ Doer *user_model.User
+ IsDoerMember bool
+ OrgID int64
+}
+
+func (opts FindOrgMembersOpts) PublicOnly() bool {
+ return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
+}
+
+// applyTeamMatesOnlyFilter make sure restricted users only see public team members and there own team mates
+func (opts FindOrgMembersOpts) applyTeamMatesOnlyFilter(sess *xorm.Session) {
+ if opts.Doer != nil && opts.IsDoerMember && opts.Doer.IsRestricted {
+ teamMates := builder.Select("DISTINCT team_user.uid").
+ From("team_user").
+ Where(builder.In("team_user.team_id", getUserTeamIDsQueryBuilder(opts.OrgID, opts.Doer.ID))).
+ And(builder.Eq{"team_user.org_id": opts.OrgID})
+
+ sess.And(
+ builder.In("org_user.uid", teamMates).
+ Or(builder.Eq{"org_user.is_public": true}),
+ )
+ }
}
// CountOrgMembers counts the organization's members
func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, error) {
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
- if opts.PublicOnly {
- sess.And("is_public = ?", true)
+ if opts.PublicOnly() {
+ sess = sess.And("is_public = ?", true)
+ } else {
+ opts.applyTeamMatesOnlyFilter(sess)
}
+
return sess.Count(new(OrgUser))
}
@@ -440,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.
@@ -508,26 +490,15 @@ 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)
- if opts.PublicOnly {
- sess.And("is_public = ?", true)
+ if opts.PublicOnly() {
+ sess = sess.And("is_public = ?", true)
+ } else {
+ opts.applyTeamMatesOnlyFilter(sess)
}
+
if opts.ListOptions.PageSize > 0 {
sess = db.SetSessionPagination(sess, opts)
@@ -656,6 +627,15 @@ func (org *Organization) getUserTeamIDs(ctx context.Context, userID int64) ([]in
Find(&teamIDs)
}
+func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
+ return builder.Select("team.id").From("team").
+ InnerJoin("team_user", "team_user.team_id = team.id").
+ Where(builder.Eq{
+ "team_user.org_id": orgID,
+ "team_user.uid": userID,
+ })
+}
+
// TeamsWithAccessToRepo returns all teams that have given access level to the repository.
func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) {
return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode)
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 23ef22e2fb..7159f0fc46 100644
--- a/models/organization/org_test.go
+++ b/models/organization/org_test.go
@@ -4,6 +4,8 @@
package organization_test
import (
+ "slices"
+ "sort"
"testing"
"code.gitea.io/gitea/models/db"
@@ -103,7 +105,7 @@ func TestUser_GetTeams(t *testing.T) {
func TestUser_GetMembers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
- members, _, err := org.GetMembers(db.DefaultContext)
+ members, _, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true})
assert.NoError(t, err)
if assert.Len(t, members, 3) {
assert.Equal(t, int64(2), members[0].ID)
@@ -127,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) {
@@ -180,67 +173,114 @@ func TestIsPublicMembership(t *testing.T) {
test(unittest.NonexistentID, unittest.NonexistentID, false)
}
-func TestFindOrgs(t *testing.T) {
+func TestRestrictedUserOrgMembers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
- UserID: 4,
- IncludePrivate: true,
+ restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ ID: 29,
+ IsRestricted: true,
})
- assert.NoError(t, err)
- if assert.Len(t, orgs, 1) {
- assert.EqualValues(t, 3, orgs[0].ID)
+ if !assert.True(t, restrictedUser.IsRestricted) {
+ return // ensure fixtures return restricted user
}
- orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
- UserID: 4,
- IncludePrivate: false,
- })
- assert.NoError(t, err)
- assert.Len(t, orgs, 0)
+ testCases := []struct {
+ name string
+ opts *organization.FindOrgMembersOpts
+ expectedUIDs []int64
+ }{
+ {
+ name: "restricted user sees public members and teammates",
+ opts: &organization.FindOrgMembersOpts{
+ OrgID: 17, // org17 where user29 is in team9
+ Doer: restrictedUser,
+ IsDoerMember: true,
+ },
+ expectedUIDs: []int64{2, 15, 20, 29}, // Public members (2) + teammates in team9 (15, 20, 29)
+ },
+ {
+ name: "restricted user sees only public members when not member",
+ opts: &organization.FindOrgMembersOpts{
+ OrgID: 3, // org3 where user29 is not a member
+ Doer: restrictedUser,
+ },
+ expectedUIDs: []int64{2, 28}, // Only public members
+ },
+ {
+ name: "non logged in only shows public members",
+ opts: &organization.FindOrgMembersOpts{
+ OrgID: 3,
+ },
+ expectedUIDs: []int64{2, 28}, // Only public members
+ },
+ {
+ name: "non restricted user sees all members",
+ opts: &organization.FindOrgMembersOpts{
+ OrgID: 17,
+ Doer: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}),
+ IsDoerMember: true,
+ },
+ expectedUIDs: []int64{2, 15, 18, 20, 29}, // All members
+ },
+ }
- total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
- UserID: 4,
- IncludePrivate: true,
- })
- assert.NoError(t, err)
- assert.EqualValues(t, 1, total)
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ count, err := organization.CountOrgMembers(db.DefaultContext, tc.opts)
+ assert.NoError(t, err)
+ assert.EqualValues(t, len(tc.expectedUIDs), count)
+
+ members, err := organization.GetOrgUsersByOrgID(db.DefaultContext, tc.opts)
+ assert.NoError(t, err)
+ memberUIDs := make([]int64, 0, len(members))
+ for _, member := range members {
+ memberUIDs = append(memberUIDs, member.UID)
+ }
+ slices.Sort(memberUIDs)
+ assert.EqualValues(t, tc.expectedUIDs, memberUIDs)
+ })
+ }
}
func TestGetOrgUsersByOrgID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- orgUsers, err := organization.GetOrgUsersByOrgID(db.DefaultContext, &organization.FindOrgMembersOpts{
- ListOptions: db.ListOptions{},
- OrgID: 3,
- PublicOnly: false,
- })
- assert.NoError(t, err)
- if assert.Len(t, orgUsers, 3) {
- assert.Equal(t, organization.OrgUser{
- ID: orgUsers[0].ID,
- OrgID: 3,
- UID: 2,
- IsPublic: true,
- }, *orgUsers[0])
- assert.Equal(t, organization.OrgUser{
- ID: orgUsers[1].ID,
- OrgID: 3,
- UID: 4,
- IsPublic: false,
- }, *orgUsers[1])
- assert.Equal(t, organization.OrgUser{
- ID: orgUsers[2].ID,
- OrgID: 3,
- UID: 28,
- IsPublic: true,
- }, *orgUsers[2])
+ opts := &organization.FindOrgMembersOpts{
+ Doer: &user_model.User{IsAdmin: true},
+ OrgID: 3,
}
+ assert.False(t, opts.PublicOnly())
+ orgUsers, err := organization.GetOrgUsersByOrgID(db.DefaultContext, opts)
+ assert.NoError(t, err)
+ sort.Slice(orgUsers, func(i, j int) bool {
+ return orgUsers[i].ID < orgUsers[j].ID
+ })
+ assert.EqualValues(t, []*organization.OrgUser{{
+ ID: 1,
+ OrgID: 3,
+ UID: 2,
+ IsPublic: true,
+ }, {
+ ID: 2,
+ OrgID: 3,
+ UID: 4,
+ IsPublic: false,
+ }, {
+ ID: 9,
+ OrgID: 3,
+ UID: 28,
+ IsPublic: true,
+ }}, orgUsers)
+
+ opts = &organization.FindOrgMembersOpts{OrgID: 3}
+ assert.True(t, opts.PublicOnly())
+ orgUsers, err = organization.GetOrgUsersByOrgID(db.DefaultContext, opts)
+ assert.NoError(t, err)
+ assert.Len(t, orgUsers, 2)
orgUsers, err = organization.GetOrgUsersByOrgID(db.DefaultContext, &organization.FindOrgMembersOpts{
ListOptions: db.ListOptions{},
OrgID: unittest.NonexistentID,
- PublicOnly: false,
})
assert.NoError(t, err)
assert.Len(t, orgUsers, 0)
diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go
index cf7acdf83b..55abb63203 100644
--- a/models/organization/org_user_test.go
+++ b/models/organization/org_user_test.go
@@ -94,7 +94,7 @@ func TestUserListIsPublicMember(t *testing.T) {
func testUserListIsPublicMember(t *testing.T, orgID int64, expected map[int64]bool) {
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
assert.NoError(t, err)
- _, membersIsPublic, err := org.GetMembers(db.DefaultContext)
+ _, membersIsPublic, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true})
assert.NoError(t, err)
assert.Equal(t, expected, membersIsPublic)
}
@@ -121,7 +121,7 @@ func TestUserListIsUserOrgOwner(t *testing.T) {
func testUserListIsUserOrgOwner(t *testing.T, orgID int64, expected map[int64]bool) {
org, err := organization.GetOrgByID(db.DefaultContext, orgID)
assert.NoError(t, err)
- members, _, err := org.GetMembers(db.DefaultContext)
+ members, _, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true})
assert.NoError(t, err)
assert.Equal(t, expected, organization.IsUserOrgOwner(db.DefaultContext, members, orgID))
}
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/fork.go b/models/repo/fork.go
index 07cd31c269..1c75e86458 100644
--- a/models/repo/fork.go
+++ b/models/repo/fork.go
@@ -54,21 +54,6 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error)
return &forkedRepo, nil
}
-// GetForks returns all the forks of the repository
-func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) {
- sess := db.GetEngine(ctx)
-
- var forks []*Repository
- if listOptions.Page == 0 {
- forks = make([]*Repository, 0, repo.NumForks)
- } else {
- forks = make([]*Repository, 0, listOptions.PageSize)
- sess = db.SetSessionPagination(sess, &listOptions)
- }
-
- return forks, sess.Find(&forks, &Repository{ForkID: repo.ID})
-}
-
// IncrementRepoForkNum increment repository fork number
func IncrementRepoForkNum(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID)
diff --git a/models/repo/pushmirror.go b/models/repo/pushmirror.go
index bf134abfb1..55e8f3a068 100644
--- a/models/repo/pushmirror.go
+++ b/models/repo/pushmirror.go
@@ -9,15 +9,13 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
-// ErrPushMirrorNotExist mirror does not exist error
-var ErrPushMirrorNotExist = util.NewNotExistErrorf("PushMirror does not exist")
-
// PushMirror represents mirror information of a repository.
type PushMirror struct {
ID int64 `xorm:"pk autoincr"`
@@ -96,26 +94,46 @@ func DeletePushMirrors(ctx context.Context, opts PushMirrorOptions) error {
return util.NewInvalidArgumentErrorf("repoID required and must be set")
}
+type findPushMirrorOptions struct {
+ db.ListOptions
+ RepoID int64
+ SyncOnCommit optional.Option[bool]
+}
+
+func (opts findPushMirrorOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.RepoID > 0 {
+ cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+ }
+ if opts.SyncOnCommit.Has() {
+ cond = cond.And(builder.Eq{"sync_on_commit": opts.SyncOnCommit.Value()})
+ }
+ return cond
+}
+
// GetPushMirrorsByRepoID returns push-mirror information of a repository.
func GetPushMirrorsByRepoID(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*PushMirror, int64, error) {
- sess := db.GetEngine(ctx).Where("repo_id = ?", repoID)
- if listOptions.Page != 0 {
- sess = db.SetSessionPagination(sess, &listOptions)
- mirrors := make([]*PushMirror, 0, listOptions.PageSize)
- count, err := sess.FindAndCount(&mirrors)
- return mirrors, count, err
+ return db.FindAndCount[PushMirror](ctx, findPushMirrorOptions{
+ ListOptions: listOptions,
+ RepoID: repoID,
+ })
+}
+
+func GetPushMirrorByIDAndRepoID(ctx context.Context, id, repoID int64) (*PushMirror, bool, error) {
+ var pushMirror PushMirror
+ has, err := db.GetEngine(ctx).Where("id = ?", id).And("repo_id = ?", repoID).Get(&pushMirror)
+ if !has || err != nil {
+ return nil, has, err
}
- mirrors := make([]*PushMirror, 0, 10)
- count, err := sess.FindAndCount(&mirrors)
- return mirrors, count, err
+ return &pushMirror, true, nil
}
// GetPushMirrorsSyncedOnCommit returns push-mirrors for this repo that should be updated by new commits
func GetPushMirrorsSyncedOnCommit(ctx context.Context, repoID int64) ([]*PushMirror, error) {
- mirrors := make([]*PushMirror, 0, 10)
- return mirrors, db.GetEngine(ctx).
- Where("repo_id = ? AND sync_on_commit = ?", repoID, true).
- Find(&mirrors)
+ return db.Find[PushMirror](ctx, findPushMirrorOptions{
+ RepoID: repoID,
+ SyncOnCommit: optional.Some(true),
+ })
}
// PushMirrorsIterate iterates all push-mirror repositories.
diff --git a/models/repo/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_list.go b/models/repo/repo_list.go
index 1bffadbf0a..9bed2e9197 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -98,8 +98,7 @@ func (repos RepositoryList) IDs() []int64 {
return repoIDs
}
-// LoadAttributes loads the attributes for the given RepositoryList
-func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
+func (repos RepositoryList) LoadOwners(ctx context.Context) error {
if len(repos) == 0 {
return nil
}
@@ -107,10 +106,6 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) {
return repo.OwnerID, true
})
- repoIDs := make([]int64, len(repos))
- for i := range repos {
- repoIDs[i] = repos[i].ID
- }
// Load owners.
users := make(map[int64]*user_model.User, len(userIDs))
@@ -123,12 +118,19 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
for i := range repos {
repos[i].Owner = users[repos[i].OwnerID]
}
+ return nil
+}
+
+func (repos RepositoryList) LoadLanguageStats(ctx context.Context) error {
+ if len(repos) == 0 {
+ return nil
+ }
// Load primary language.
stats := make(LanguageStatList, 0, len(repos))
if err := db.GetEngine(ctx).
Where("`is_primary` = ? AND `language` != ?", true, "other").
- In("`repo_id`", repoIDs).
+ In("`repo_id`", repos.IDs()).
Find(&stats); err != nil {
return fmt.Errorf("find primary languages: %w", err)
}
@@ -141,10 +143,18 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
}
}
}
-
return nil
}
+// LoadAttributes loads the attributes for the given RepositoryList
+func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
+ if err := repos.LoadOwners(ctx); err != nil {
+ return err
+ }
+
+ return repos.LoadLanguageStats(ctx)
+}
+
// SearchRepoOptions holds the search options
type SearchRepoOptions struct {
db.ListOptions
diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go
index c13b698abf..6468e0f605 100644
--- a/models/repo/repo_test.go
+++ b/models/repo/repo_test.go
@@ -1,13 +1,12 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
-package repo_test
+package repo
import (
"testing"
"code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -20,18 +19,18 @@ import (
)
var (
- countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10}
- countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
- countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
+ countRepospts = CountRepositoryOptions{OwnerID: 10}
+ countReposptsPublic = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
+ countReposptsPrivate = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
)
func TestGetRepositoryCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := db.DefaultContext
- count, err1 := repo_model.CountRepositories(ctx, countRepospts)
- privateCount, err2 := repo_model.CountRepositories(ctx, countReposptsPrivate)
- publicCount, err3 := repo_model.CountRepositories(ctx, countReposptsPublic)
+ count, err1 := CountRepositories(ctx, countRepospts)
+ privateCount, err2 := CountRepositories(ctx, countReposptsPrivate)
+ publicCount, err3 := CountRepositories(ctx, countReposptsPublic)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
@@ -42,7 +41,7 @@ func TestGetRepositoryCount(t *testing.T) {
func TestGetPublicRepositoryCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPublic)
+ count, err := CountRepositories(db.DefaultContext, countReposptsPublic)
assert.NoError(t, err)
assert.Equal(t, int64(1), count)
}
@@ -50,14 +49,14 @@ func TestGetPublicRepositoryCount(t *testing.T) {
func TestGetPrivateRepositoryCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPrivate)
+ count, err := CountRepositories(db.DefaultContext, countReposptsPrivate)
assert.NoError(t, err)
assert.Equal(t, int64(2), count)
}
func TestRepoAPIURL(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
+ repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 10})
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL())
}
@@ -65,22 +64,22 @@ func TestRepoAPIURL(t *testing.T) {
func TestWatchRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true))
- unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
- unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
+ assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, true))
+ unittest.AssertExistsAndLoadBean(t, &Watch{RepoID: repo.ID, UserID: user.ID})
+ unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID})
- assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false))
- unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
- unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
+ assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, false))
+ unittest.AssertNotExistsBean(t, &Watch{RepoID: repo.ID, UserID: user.ID})
+ unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID})
}
func TestMetas(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- repo := &repo_model.Repository{Name: "testRepo"}
+ repo := &Repository{Name: "testRepo"}
repo.Owner = &user_model.User{Name: "testOwner"}
repo.OwnerName = repo.Owner.Name
@@ -90,16 +89,16 @@ func TestMetas(t *testing.T) {
assert.Equal(t, "testRepo", metas["repo"])
assert.Equal(t, "testOwner", metas["user"])
- externalTracker := repo_model.RepoUnit{
+ externalTracker := RepoUnit{
Type: unit.TypeExternalTracker,
- Config: &repo_model.ExternalTrackerConfig{
+ Config: &ExternalTrackerConfig{
ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}",
},
}
testSuccess := func(expectedStyle string) {
- repo.Units = []*repo_model.RepoUnit{&externalTracker}
- repo.RenderingMetas = nil
+ repo.Units = []*RepoUnit{&externalTracker}
+ repo.commonRenderingMetas = nil
metas := repo.ComposeMetas(db.DefaultContext)
assert.Equal(t, expectedStyle, metas["style"])
assert.Equal(t, "testRepo", metas["repo"])
@@ -118,7 +117,7 @@ func TestMetas(t *testing.T) {
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp
testSuccess(markup.IssueNameStyleRegexp)
- repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 3)
+ repo, err := GetRepositoryByID(db.DefaultContext, 3)
assert.NoError(t, err)
metas = repo.ComposeMetas(db.DefaultContext)
@@ -132,7 +131,7 @@ func TestGetRepositoryByURL(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("InvalidPath", func(t *testing.T) {
- repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, "something")
+ repo, err := GetRepositoryByURL(db.DefaultContext, "something")
assert.Nil(t, repo)
assert.Error(t, err)
@@ -140,7 +139,7 @@ func TestGetRepositoryByURL(t *testing.T) {
t.Run("ValidHttpURL", func(t *testing.T) {
test := func(t *testing.T, url string) {
- repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
+ repo, err := GetRepositoryByURL(db.DefaultContext, url)
assert.NotNil(t, repo)
assert.NoError(t, err)
@@ -155,7 +154,7 @@ func TestGetRepositoryByURL(t *testing.T) {
t.Run("ValidGitSshURL", func(t *testing.T) {
test := func(t *testing.T, url string) {
- repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
+ repo, err := GetRepositoryByURL(db.DefaultContext, url)
assert.NotNil(t, repo)
assert.NoError(t, err)
@@ -173,7 +172,7 @@ func TestGetRepositoryByURL(t *testing.T) {
t.Run("ValidImplicitSshURL", func(t *testing.T) {
test := func(t *testing.T, url string) {
- repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
+ repo, err := GetRepositoryByURL(db.DefaultContext, url)
assert.NotNil(t, repo)
assert.NoError(t, err)
@@ -200,21 +199,21 @@ func TestComposeSSHCloneURL(t *testing.T) {
setting.SSH.Domain = "domain"
setting.SSH.Port = 22
setting.Repository.UseCompatSSHURI = false
- assert.Equal(t, "git@domain:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
+ assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL("user", "repo"))
setting.Repository.UseCompatSSHURI = true
- assert.Equal(t, "ssh://git@domain/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
+ assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL("user", "repo"))
// test SSH_DOMAIN while use non-standard SSH port
setting.SSH.Port = 123
setting.Repository.UseCompatSSHURI = false
- assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
+ assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
setting.Repository.UseCompatSSHURI = true
- assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
+ assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
// test IPv6 SSH_DOMAIN
setting.Repository.UseCompatSSHURI = false
setting.SSH.Domain = "::1"
setting.SSH.Port = 22
- assert.Equal(t, "git@[::1]:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
+ assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL("user", "repo"))
setting.SSH.Port = 123
- assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
+ assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
}
diff --git a/models/repo/search.go b/models/repo/search.go
index a73d9fc215..ffb8e26745 100644
--- a/models/repo/search.go
+++ b/models/repo/search.go
@@ -36,6 +36,7 @@ var OrderByMap = map[string]map[string]db.SearchOrderBy{
var OrderByFlatMap = map[string]db.SearchOrderBy{
"newest": OrderByMap["desc"]["created"],
"oldest": OrderByMap["asc"]["created"],
+ "recentupdate": OrderByMap["desc"]["updated"],
"leastupdate": OrderByMap["asc"]["updated"],
"reversealphabetically": OrderByMap["desc"]["alpha"],
"alphabetically": OrderByMap["asc"]["alpha"],
diff --git a/models/unit/unit.go b/models/unit/unit.go
index 3b62e5f982..c816fc6c68 100644
--- a/models/unit/unit.go
+++ b/models/unit/unit.go
@@ -80,6 +80,27 @@ var (
TypePullRequests,
}
+ // DefaultMirrorRepoUnits contains the default unit types for mirrors
+ DefaultMirrorRepoUnits = []Type{
+ TypeCode,
+ TypeIssues,
+ TypeReleases,
+ TypeWiki,
+ TypeProjects,
+ TypePackages,
+ }
+
+ // DefaultTemplateRepoUnits contains the default unit types for templates
+ DefaultTemplateRepoUnits = []Type{
+ TypeCode,
+ TypeIssues,
+ TypePullRequests,
+ TypeReleases,
+ TypeWiki,
+ TypeProjects,
+ TypePackages,
+ }
+
// NotAllowedDefaultRepoUnits contains units that can't be default
NotAllowedDefaultRepoUnits = []Type{
TypeExternalWiki,
@@ -147,6 +168,7 @@ func LoadUnitConfig() error {
if len(DefaultRepoUnits) == 0 {
return errors.New("no default repository units found")
}
+ // default fork repo units
setDefaultForkRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultForkRepoUnits...)
if len(invalidKeys) > 0 {
log.Warn("Invalid keys in default fork repo units: %s", strings.Join(invalidKeys, ", "))
@@ -155,6 +177,24 @@ func LoadUnitConfig() error {
if len(DefaultForkRepoUnits) == 0 {
return errors.New("no default fork repository units found")
}
+ // default mirror repo units
+ setDefaultMirrorRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultMirrorRepoUnits...)
+ if len(invalidKeys) > 0 {
+ log.Warn("Invalid keys in default mirror repo units: %s", strings.Join(invalidKeys, ", "))
+ }
+ DefaultMirrorRepoUnits = validateDefaultRepoUnits(DefaultMirrorRepoUnits, setDefaultMirrorRepoUnits)
+ if len(DefaultMirrorRepoUnits) == 0 {
+ return errors.New("no default mirror repository units found")
+ }
+ // default template repo units
+ setDefaultTemplateRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultTemplateRepoUnits...)
+ if len(invalidKeys) > 0 {
+ log.Warn("Invalid keys in default template repo units: %s", strings.Join(invalidKeys, ", "))
+ }
+ DefaultTemplateRepoUnits = validateDefaultRepoUnits(DefaultTemplateRepoUnits, setDefaultTemplateRepoUnits)
+ if len(DefaultTemplateRepoUnits) == 0 {
+ return errors.New("no default template repository units found")
+ }
return nil
}
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/models/user/user.go b/models/user/user.go
index c1cb988e43..a2d9166291 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -48,19 +48,19 @@ const (
UserTypeIndividual UserType = iota // Historic reason to make it starts at 0.
// UserTypeOrganization defines an organization
- UserTypeOrganization
+ UserTypeOrganization // 1
// UserTypeUserReserved reserves a (non-existing) user, i.e. to prevent a spam user from re-registering after being deleted, or to reserve the name until the user is actually created later on
- UserTypeUserReserved
+ UserTypeUserReserved // 2
// UserTypeOrganizationReserved reserves a (non-existing) organization, to be used in combination with UserTypeUserReserved
- UserTypeOrganizationReserved
+ UserTypeOrganizationReserved // 3
// UserTypeBot defines a bot user
- UserTypeBot
+ UserTypeBot // 4
// UserTypeRemoteUser defines a remote user for federated users
- UserTypeRemoteUser
+ UserTypeRemoteUser // 5
)
const (
@@ -884,7 +884,13 @@ func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
// GetInactiveUsers gets all inactive users
func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) {
- var cond builder.Cond = builder.Eq{"is_active": false}
+ cond := builder.And(
+ builder.Eq{"is_active": false},
+ builder.Or( // only plain user
+ builder.Eq{"`type`": UserTypeIndividual},
+ builder.Eq{"`type`": UserTypeUserReserved},
+ ),
+ )
if olderThan > 0 {
cond = cond.And(builder.Lt{"created_unix": time.Now().Add(-olderThan).Unix()})
diff --git a/models/user/user_test.go b/models/user/user_test.go
index bc1abc6451..6701be39a5 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -588,3 +588,17 @@ func TestDisabledUserFeatures(t *testing.T) {
assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f))
}
}
+
+func TestGetInactiveUsers(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ // all inactive users
+ // user1's createdunix is 1730468968
+ users, err := user_model.GetInactiveUsers(db.DefaultContext, 0)
+ assert.NoError(t, err)
+ assert.Len(t, users, 1)
+ interval := time.Now().Unix() - 1730468968 + 3600*24
+ users, err = user_model.GetInactiveUsers(db.DefaultContext, time.Duration(interval*int64(time.Second)))
+ assert.NoError(t, err)
+ assert.Len(t, users, 0)
+}
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 9e43030f40..928c80700b 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -147,6 +147,9 @@ func StringsToInt64s(strs []string) ([]int64, error) {
}
ints := make([]int64, 0, len(strs))
for _, s := range strs {
+ if s == "" {
+ continue
+ }
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return nil, err
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index 4af8b9bc4d..86cccdf209 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -152,6 +152,7 @@ func TestStringsToInt64s(t *testing.T) {
}
testSuccess(nil, nil)
testSuccess([]string{}, []int64{})
+ testSuccess([]string{""}, []int64{})
testSuccess([]string{"-1234"}, []int64{-1234})
testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})
diff --git a/modules/container/set.go b/modules/container/set.go
index adb77dcac7..105533f203 100644
--- a/modules/container/set.go
+++ b/modules/container/set.go
@@ -31,8 +31,8 @@ func (s Set[T]) AddMultiple(values ...T) {
}
}
-// Contains determines whether a set contains the specified elements.
-// Returns true if the set contains the specified element; otherwise, false.
+// Contains determines whether a set contains all these elements.
+// Returns true if the set contains all these elements; otherwise, false.
func (s Set[T]) Contains(values ...T) bool {
ret := true
for _, value := range values {
diff --git a/modules/container/set_test.go b/modules/container/set_test.go
index 1502236034..a8b7ff8190 100644
--- a/modules/container/set_test.go
+++ b/modules/container/set_test.go
@@ -18,7 +18,9 @@ func TestSet(t *testing.T) {
assert.True(t, s.Contains("key1"))
assert.True(t, s.Contains("key2"))
+ assert.True(t, s.Contains("key1", "key2"))
assert.False(t, s.Contains("key3"))
+ assert.False(t, s.Contains("key1", "key3"))
assert.True(t, s.Remove("key2"))
assert.False(t, s.Contains("key2"))
diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go
index 3b1a466b2e..7dfda72155 100644
--- a/modules/git/batch_reader.go
+++ b/modules/git/batch_reader.go
@@ -146,9 +146,8 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi
}
// ReadBatchLine reads the header line from cat-file --batch
-// We expect:
-// {TextBefore} {TextAfter} 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?
+ ``)
- if err != nil {
- return
- }
-
- // include language-x class as part of commonmark spec
- // the "display" class is used by "js/markup/math.js" to render the code element as a block
- _, err = w.WriteString(`
")
- if err != nil {
- return
- }
- }
- }),
- ),
- math.NewExtension(
- math.Enabled(setting.Markdown.EnableMath),
- ),
- meta.Meta,
- ),
- goldmark.WithParserOptions(
- parser.WithAttribute(),
- parser.WithAutoHeadingID(),
- parser.WithASTTransformers(
- util.Prioritized(NewASTTransformer(), 10000),
- ),
- ),
- goldmark.WithRendererOptions(
- html.WithUnsafe(),
- ),
- )
-
- // Override the original Tasklist renderer!
- specMarkdown.Renderer().AddOptions(
- renderer.WithNodeRenderers(
- util.Prioritized(NewHTMLRenderer(), 10),
- ),
- )
- })
- return specMarkdown
+ goldmarkMarkdown goldmark.Markdown
}
-// actualRender renders Markdown to HTML without handling special links.
-func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
- converter := SpecializedMarkdown()
+func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error {
+ return r.goldmarkMarkdown.Convert(source, writer, opts...)
+}
+
+func (r *GlodmarkRender) Renderer() renderer.Renderer {
+ return r.goldmarkMarkdown.Renderer()
+}
+
+func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
+ if entering {
+ language, _ := c.Language()
+ if language == nil {
+ language = []byte("text")
+ }
+
+ languageStr := string(language)
+
+ preClasses := []string{"code-block"}
+ if languageStr == "mermaid" || languageStr == "math" {
+ preClasses = append(preClasses, "is-loading")
+ }
+
+ err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``)
- if err != nil {
- return
- }
- } else {
- _, err := w.WriteString("
`, strings.Join(preClasses, " "))
+ if err != nil {
+ return
+ }
+
+ // include language-x class as part of commonmark spec
+ // the "display" class is used by "js/markup/math.js" to render the code element as a block
+ err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, `
")
+ if err != nil {
+ return
+ }
+ }
+}
+
+// SpecializedMarkdown sets up the Gitea specific markdown extensions
+func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
+ // TODO: it could use a pool to cache the renderers to reuse them with different contexts
+ // at the moment it is fast enough (see the benchmarks)
+ r := &GlodmarkRender{ctx: ctx}
+ r.goldmarkMarkdown = goldmark.New(
+ goldmark.WithExtensions(
+ extension.NewTable(extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
+ extension.Strikethrough,
+ extension.TaskList,
+ extension.DefinitionList,
+ common.FootnoteExtension,
+ highlighting.NewHighlighting(
+ highlighting.WithFormatOptions(
+ chromahtml.WithClasses(true),
+ chromahtml.PreventSurroundingPre(true),
+ ),
+ highlighting.WithWrapperRenderer(r.highlightingRenderer),
+ ),
+ math.NewExtension(&ctx.RenderInternal, math.Enabled(setting.Markdown.EnableMath)),
+ meta.Meta,
+ ),
+ goldmark.WithParserOptions(
+ parser.WithAttribute(),
+ parser.WithAutoHeadingID(),
+ parser.WithASTTransformers(util.Prioritized(NewASTTransformer(&ctx.RenderInternal), 10000)),
+ ),
+ goldmark.WithRendererOptions(html.WithUnsafe()),
+ )
+
+ // Override the original Tasklist renderer!
+ r.goldmarkMarkdown.Renderer().AddOptions(
+ renderer.WithNodeRenderers(util.Prioritized(NewHTMLRenderer(&ctx.RenderInternal), 10)),
+ )
+
+ return r
+}
+
+// render calls goldmark render to convert Markdown to HTML
+// NOTE: The output of this method MUST get sanitized separately!!!
+func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
+ converter := SpecializedMarkdown(ctx)
lw := &limitWriter{
w: output,
limit: setting.UI.MaxDisplayFileSize * 3,
@@ -160,8 +164,8 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
}
log.Warn("Unable to render markdown due to panic in goldmark: %v", err)
- if log.IsDebug() {
- log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2))
+ if (!setting.IsProd && !setting.IsInTesting) || log.IsDebug() {
+ log.Error("Panic in markdown: %v\n%s", err, log.Stack(2))
}
}()
@@ -200,26 +204,6 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
return nil
}
-// Note: The output of this method must get sanitized.
-func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
- defer func() {
- err := recover()
- if err == nil {
- return
- }
-
- log.Warn("Unable to render markdown due to panic in goldmark - will return raw bytes")
- if log.IsDebug() {
- log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2))
- }
- _, err = io.Copy(output, input)
- if err != nil {
- log.Error("io.Copy failed: %v", err)
- }
- }()
- return actualRender(ctx, input, output)
-}
-
// MarkupName describes markup's name
var MarkupName = "markdown"
@@ -257,9 +241,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..e4889a75e5 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.com
`, string(language))
+ if err != nil {
+ return
+ }
+ } else {
+ _, err := w.WriteString("
-https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+88fc37a3c0...12fc37a3c0 (hash)
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
-https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+88fc37a3c0
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
ðŸ‘
mail@domain.com
@@ -685,9 +689,9 @@ space
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
88fc37a3c0...12fc37a3c0 (hash)
88fc37a3c0
`)
+ _ = r.renderInternal.FormatWithSafeAttrs(w, ``)
r.writeLines(w, source, n)
} else {
_, _ = w.WriteString(`
` + "\n")
diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go
index 96848099cc..0cff4f1e74 100644
--- a/modules/markup/markdown/math/inline_renderer.go
+++ b/modules/markup/markdown/math/inline_renderer.go
@@ -6,17 +6,21 @@ package math
import (
"bytes"
+ "code.gitea.io/gitea/modules/markup/internal"
+
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
// InlineRenderer is an inline renderer
-type InlineRenderer struct{}
+type InlineRenderer struct {
+ renderInternal *internal.RenderInternal
+}
// NewInlineRenderer returns a new renderer for inline math
-func NewInlineRenderer() renderer.NodeRenderer {
- return &InlineRenderer{}
+func NewInlineRenderer(renderInternal *internal.RenderInternal) renderer.NodeRenderer {
+ return &InlineRenderer{renderInternal: renderInternal}
}
func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -25,7 +29,7 @@ func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Nod
if _, ok := n.(*InlineBlock); ok {
extraClass = "display "
}
- _, _ = w.WriteString(``)
+ _ = r.renderInternal.FormatWithSafeAttrs(w, ``, extraClass)
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
segment := c.(*ast.Text).Segment
value := util.EscapeHTML(segment.Value(source))
diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go
index 3d9f376bc6..7e8defcd4a 100644
--- a/modules/markup/markdown/math/math.go
+++ b/modules/markup/markdown/math/math.go
@@ -4,6 +4,8 @@
package math
import (
+ "code.gitea.io/gitea/modules/markup/internal"
+
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
@@ -12,6 +14,7 @@ import (
// Extension is a math extension
type Extension struct {
+ renderInternal *internal.RenderInternal
enabled bool
parseDollarInline bool
parseDollarBlock bool
@@ -39,38 +42,10 @@ func Enabled(enable ...bool) Option {
})
}
-// WithInlineDollarParser enables or disables the parsing of $...$
-func WithInlineDollarParser(enable ...bool) Option {
- value := true
- if len(enable) > 0 {
- value = enable[0]
- }
- return extensionFunc(func(e *Extension) {
- e.parseDollarInline = value
- })
-}
-
-// WithBlockDollarParser enables or disables the parsing of $$...$$
-func WithBlockDollarParser(enable ...bool) Option {
- value := true
- if len(enable) > 0 {
- value = enable[0]
- }
- return extensionFunc(func(e *Extension) {
- e.parseDollarBlock = value
- })
-}
-
-// Math represents a math extension with default rendered delimiters
-var Math = &Extension{
- enabled: true,
- parseDollarBlock: true,
- parseDollarInline: true,
-}
-
// NewExtension creates a new math extension with the provided options
-func NewExtension(opts ...Option) *Extension {
+func NewExtension(renderInternal *internal.RenderInternal, opts ...Option) *Extension {
r := &Extension{
+ renderInternal: renderInternal,
enabled: true,
parseDollarBlock: true,
parseDollarInline: true,
@@ -102,7 +77,7 @@ func (e *Extension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithInlineParsers(inlines...))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
- util.Prioritized(NewBlockRenderer(), 501),
- util.Prioritized(NewInlineRenderer(), 502),
+ util.Prioritized(NewBlockRenderer(e.renderInternal), 501),
+ util.Prioritized(NewInlineRenderer(e.renderInternal), 502),
))
}
diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go
index 6949966328..278c33f1d2 100644
--- a/modules/markup/markdown/meta_test.go
+++ b/modules/markup/markdown/meta_test.go
@@ -11,10 +11,8 @@ import (
"github.com/stretchr/testify/assert"
)
-/*
-IssueTemplate is a legacy to keep the unit tests working.
-Copied from structs.IssueTemplate, the original type has been changed a lot to support yaml template.
-*/
+// IssueTemplate is a legacy to keep the unit tests working.
+// Copied from structs.IssueTemplate, the original type has been changed a lot to support yaml template.
type IssueTemplate struct {
Name string `json:"name" yaml:"name"`
Title string `json:"title" yaml:"title"`
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
index 92dc500e69..2651d44a69 100644
--- a/modules/markup/markdown/transform_blockquote.go
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -32,7 +32,8 @@ func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast
default: // including "note"
octiconName = "info"
}
- _, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
+ svgHTML := svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)
+ _, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(svgHTML)))
}
return ast.WalkContinue, nil
}
@@ -128,13 +129,13 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
}
// color the blockquote
- v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
+ v.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("attention-header attention-"+attentionType)))
// create an emphasis to make it bold
attentionParagraph := ast.NewParagraph()
g.applyElementDir(attentionParagraph)
emphasis := ast.NewEmphasis(2)
- emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
+ emphasis.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("attention-"+attentionType)))
attentionAstString := ast.NewString([]byte(cases.Title(language.English).String(attentionType)))
diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go
index ff7d24eec9..bccc43aad2 100644
--- a/modules/markup/markdown/transform_codespan.go
+++ b/modules/markup/markdown/transform_codespan.go
@@ -5,7 +5,6 @@ package markdown
import (
"bytes"
- "fmt"
"strings"
"code.gitea.io/gitea/modules/markup"
@@ -40,7 +39,7 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
r.Writer.RawWrite(w, value)
}
case *ColorPreview:
- _, _ = w.WriteString(fmt.Sprintf(``, string(v.Color)))
+ _ = r.renderInternal.FormatWithSafeAttrs(w, ``, string(v.Color))
}
}
return ast.WalkSkipChildren, nil
diff --git a/modules/markup/markdown/transform_image.go b/modules/markup/markdown/transform_image.go
index 812e24f0a2..b2262c1c78 100644
--- a/modules/markup/markdown/transform_image.go
+++ b/modules/markup/markdown/transform_image.go
@@ -21,7 +21,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image)
// Check if the destination is a real link
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
v.Destination = []byte(giteautil.URLJoin(
- ctx.Links.ResolveMediaLink(ctx.IsWiki),
+ ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()),
strings.TrimLeft(string(v.Destination), "/"),
))
}
diff --git a/modules/markup/markdown/transform_list.go b/modules/markup/markdown/transform_list.go
index b982fd4a83..c89ad2f2cf 100644
--- a/modules/markup/markdown/transform_list.go
+++ b/modules/markup/markdown/transform_list.go
@@ -72,7 +72,7 @@ func (g *ASTTransformer) transformList(_ *markup.RenderContext, v *ast.List, rc
}
newChild := NewTaskCheckBoxListItem(listItem)
newChild.IsChecked = taskCheckBox.IsChecked
- newChild.SetAttributeString("class", []byte("task-list-item"))
+ newChild.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("task-list-item")))
segments := newChild.FirstChild().Lines()
if segments.Len() > 0 {
segment := segments.At(0)
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index 25f8d15ef4..c587a6ada5 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -144,14 +144,14 @@ func (r *Writer) resolveLink(kind, link string) string {
}
base := r.Ctx.Links.Base
- if r.Ctx.IsWiki {
+ if r.Ctx.IsMarkupContentWiki() {
base = r.Ctx.Links.WikiLink()
} else if r.Ctx.Links.HasBranchInfo() {
base = r.Ctx.Links.SrcLink()
}
if kind == "image" || kind == "video" {
- base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
+ base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki())
}
link = util.URLJoin(base, link)
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index 75b60ed81f..a3eefc3db3 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
@@ -26,7 +27,7 @@ func TestRender_StandardLinks(t *testing.T) {
Base: "/relative-path",
BranchPath: "branch/main",
},
- IsWiki: isWiki,
+ Metas: map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")},
}, input)
assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
diff --git a/modules/markup/render.go b/modules/markup/render.go
index f2ce9229af..f05cb62626 100644
--- a/modules/markup/render.go
+++ b/modules/markup/render.go
@@ -5,20 +5,19 @@ package markup
import (
"context"
- "errors"
"fmt"
"io"
"net/url"
- "path/filepath"
"strings"
- "sync"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/markup/internal"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast"
+ "golang.org/x/sync/errgroup"
)
type RenderMetaMode string
@@ -29,15 +28,37 @@ const (
RenderMetaAsTable RenderMetaMode = "table"
)
+var RenderBehaviorForTesting struct {
+ // Markdown line break rendering has 2 default behaviors:
+ // * Use hard: replace "\n" with "
" for comments, setting.Markdown.EnableHardLineBreakInComments=true
+ // * Keep soft: "\n" for non-comments (a.k.a. documents), setting.Markdown.EnableHardLineBreakInDocuments=false
+ // In history, there was a mess:
+ // * The behavior was controlled by `Metas["mode"] != "document",
+ // * However, many places render the content without setting "mode" in Metas, all these places used comment line break setting incorrectly
+ ForceHardLineBreak bool
+
+ // Gitea will emit some internal attributes for various purposes, these attributes don't affect rendering.
+ // But there are too many hard-coded test cases, to avoid changing all of them again and again, we can disable emitting these internal attributes.
+ DisableInternalAttributes bool
+}
+
// RenderContext represents a render context
type RenderContext struct {
- Ctx context.Context
- RelativePath string // relative path from tree root of the branch
- Type string
- IsWiki bool
- Links Links
- Metas map[string]string // user, repo, mode(comment/document)
- DefaultLink string
+ Ctx context.Context
+ RelativePath string // relative path from tree root of the branch
+
+ // eg: "orgmode", "asciicast", "console"
+ // for file mode, it could be left as empty, and will be detected by file extension in RelativePath
+ MarkupType string
+
+ Links Links // special link references for rendering, especially when there is a branch/tree path
+
+ // user&repo, format&style®exp (for external issue pattern), teams&org (for mention)
+ // BranchNameSubURL (for iframe&asciicast)
+ // markupAllowShortIssuePattern, markupContentMode (wiki)
+ // markdownLineBreakStyle (comment, document)
+ Metas map[string]string
+
GitRepo *git.Repository
Repo gitrepo.Repository
ShaExistCache map[string]bool
@@ -45,6 +66,8 @@ type RenderContext struct {
SidebarTocNode ast.Node
RenderMetaAs RenderMetaMode
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
+
+ RenderInternal internal.RenderInternal
}
// Cancel runs any cleanup functions that have been registered for this Ctx
@@ -75,14 +98,35 @@ func (ctx *RenderContext) AddCancel(fn func()) {
}
}
+func (ctx *RenderContext) IsMarkupContentWiki() bool {
+ return ctx.Metas != nil && ctx.Metas["markupContentMode"] == "wiki"
+}
+
// Render renders markup file to HTML with all specific handling stuff.
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
- if ctx.Type != "" {
- return renderByType(ctx, input, output)
- } else if ctx.RelativePath != "" {
- return renderFile(ctx, input, output)
+ if ctx.MarkupType == "" && ctx.RelativePath != "" {
+ ctx.MarkupType = DetectMarkupTypeByFileName(ctx.RelativePath)
+ if ctx.MarkupType == "" {
+ return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RelativePath)
+ }
}
- return errors.New("render options both filename and type missing")
+
+ renderer := renderers[ctx.MarkupType]
+ if renderer == nil {
+ return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.MarkupType)
+ }
+
+ if ctx.RelativePath != "" {
+ if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
+ if !ctx.InStandalonePage {
+ // for an external "DisplayInIFrame" render, it could only output its content in a standalone page
+ // otherwise, a