Merge branch 'main' into lunny/issue_dev

This commit is contained in:
Lunny Xiao 2024-11-28 12:14:58 -08:00
commit 3d8ed0e853
624 changed files with 15277 additions and 12415 deletions

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -63,3 +63,4 @@ Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
Yu Liu <1240335630@qq.com> (@HEREYUA)
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
hiifong <i@hiif.ong> (@hiifong)

View File

@ -377,12 +377,12 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
.PHONY: lint-js
lint-js: node_modules
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
# npx tsc
# npx vue-tsc
.PHONY: lint-js-fix
lint-js-fix: node_modules
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
# npx tsc
# npx vue-tsc
.PHONY: lint-css
lint-css: node_modules
@ -451,6 +451,10 @@ lint-templates: .venv node_modules
lint-yaml: .venv
@poetry run yamllint .
.PHONY: tsc
tsc:
npx vue-tsc
.PHONY: watch
watch:
@bash tools/watch.sh

View File

@ -1090,8 +1090,8 @@
"licenseText": "MIT License\n\nCopyright (c) 2017 Asher\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/stretchr/testify/assert",
"path": "github.com/stretchr/testify/assert/LICENSE",
"name": "github.com/stretchr/testify",
"path": "github.com/stretchr/testify/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{

View File

@ -111,13 +111,11 @@ 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
}
}
_ = private.SSHLog(ctx, true, logMsg)
}
return cli.Exit("", 1)
@ -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]

View File

@ -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
@ -1936,6 +1944,13 @@ LEVEL = Info
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
;MINIO_SECRET_ACCESS_KEY =
;;
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
;MINIO_IAM_ENDPOINT =
;;
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
;MINIO_BUCKET = gitea
;;
@ -2680,6 +2695,13 @@ LEVEL = Info
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
;MINIO_SECRET_ACCESS_KEY =
;;
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
;MINIO_IAM_ENDPOINT =
;;
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
;MINIO_BUCKET = gitea
;;

12
flake.lock generated
View File

@ -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": {

View File

@ -22,14 +22,13 @@
gzip
# frontend
nodejs_20
nodejs_22
# linting
python312
poetry
# backend
go_1_22
gofumpt
sqlite
];

5
go.mod
View File

@ -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

9
go.sum
View File

@ -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=

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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() {

View File

@ -200,7 +200,7 @@ func (a *Action) LoadActUser(ctx context.Context) {
}
}
func (a *Action) loadRepo(ctx context.Context) {
func (a *Action) LoadRepo(ctx context.Context) {
if a.Repo != nil {
return
}
@ -250,7 +250,10 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
// GetRepoUserName returns the name of the action repository owner.
func (a *Action) GetRepoUserName(ctx context.Context) string {
a.loadRepo(ctx)
a.LoadRepo(ctx)
if a.Repo == nil {
return "(non-existing-repo)"
}
return a.Repo.OwnerName
}
@ -262,7 +265,10 @@ func (a *Action) ShortRepoUserName(ctx context.Context) string {
// GetRepoName returns the name of the action repository.
func (a *Action) GetRepoName(ctx context.Context) string {
a.loadRepo(ctx)
a.LoadRepo(ctx)
if a.Repo == nil {
return "(non-existing-repo)"
}
return a.Repo.Name
}
@ -638,7 +644,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
}
if repoChanged {
act.loadRepo(ctx)
act.LoadRepo(ctx)
repo = act.Repo
// check repo owner exist.
@ -764,7 +770,7 @@ func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64)
// CountActionCreatedUnixString count actions where created_unix is an empty string
func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
if setting.Database.Type.IsSQLite3() {
return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action))
return db.GetEngine(ctx).Where(`created_unix = ''`).Count(new(Action))
}
return 0, nil
}
@ -772,7 +778,7 @@ func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
// FixActionCreatedUnixString set created_unix to zero if it is an empty string
func FixActionCreatedUnixString(ctx context.Context) (int64, error) {
if setting.Database.Type.IsSQLite3() {
res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`)
res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ''`)
if err != nil {
return 0, err
}

View File

@ -256,7 +256,7 @@ func TestConsistencyUpdateAction(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
ID: int64(id),
})
_, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = "" WHERE id = ?`, id)
_, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = '' WHERE id = ?`, id)
assert.NoError(t, err)
actions := make([]*activities_model.Action, 0, 1)
//

View File

@ -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() {

View File

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
)
@ -89,14 +90,33 @@ func (cred *WebAuthnCredential) AfterLoad() {
// WebAuthnCredentialList is a list of *WebAuthnCredential
type WebAuthnCredentialList []*WebAuthnCredential
// newCredentialFlagsFromAuthenticatorFlags is copied from https://github.com/go-webauthn/webauthn/pull/337
// to convert protocol.AuthenticatorFlags to webauthn.CredentialFlags
func newCredentialFlagsFromAuthenticatorFlags(flags protocol.AuthenticatorFlags) webauthn.CredentialFlags {
return webauthn.CredentialFlags{
UserPresent: flags.HasUserPresent(),
UserVerified: flags.HasUserVerified(),
BackupEligible: flags.HasBackupEligible(),
BackupState: flags.HasBackupState(),
}
}
// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
func (list WebAuthnCredentialList) ToCredentials(defaultAuthFlags ...protocol.AuthenticatorFlags) []webauthn.Credential {
// TODO: at the moment, Gitea doesn't store or check the flags
// so we need to use the default flags from the authenticator to make the login validation pass
// In the future, we should:
// 1. store the flags when registering the credential
// 2. provide the stored flags when converting the credentials (for login)
// 3. for old users, still use this fallback to the default flags
defAuthFlags := util.OptionalArg(defaultAuthFlags)
creds := make([]webauthn.Credential, 0, len(list))
for _, cred := range list {
creds = append(creds, webauthn.Credential{
ID: cred.CredentialID,
PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType,
Flags: newCredentialFlagsFromAuthenticatorFlags(defAuthFlags),
Authenticator: webauthn.Authenticator{
AAGUID: cred.AAGUID,
SignCount: cred.SignCount,

View File

@ -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 {

View File

@ -134,6 +134,9 @@ func SyncAllTables() error {
func InitEngine(ctx context.Context) error {
xormEngine, err := newXORMEngine()
if err != nil {
if strings.Contains(err.Error(), "SQLite3 support") {
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
}
return fmt.Errorf("failed to connect to database: %w", err)
}

View File

@ -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

View File

@ -11,7 +11,7 @@
id: 2
oid: 2eccdb43825d2a49d99d542daa20075cff1d97d9d2349a8977efe9c03661737c
size: 107
size: 2048
repository_id: 54
created_unix: 1671607299

View File

@ -129,3 +129,9 @@
uid: 2
org_id: 35
is_public: true
-
id: 23
uid: 20
org_id: 17
is_public: false

View File

@ -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

View File

@ -1,7 +1,7 @@
-
id: 1
setting_key: 'picture.disable_gravatar'
setting_value: 'false'
setting_value: 'true'
version: 1
created: 1653533198
updated: 1653533198

View File

@ -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

View File

@ -34,6 +34,7 @@ type ProtectedBranch struct {
RepoID int64 `xorm:"UNIQUE(s)"`
Repo *repo_model.Repository `xorm:"-"`
RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
globRule glob.Glob `xorm:"-"`
isPlainName bool `xorm:"-"`
CanPush bool `xorm:"NOT NULL DEFAULT false"`
@ -413,14 +414,27 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
}
protectBranch.ApprovalsWhitelistTeamIDs = whitelist
// Make sure protectBranch.ID is not 0 for whitelists
// Looks like it's a new rule
if protectBranch.ID == 0 {
// as it's a new rule and if priority was not set, we need to calc it.
if protectBranch.Priority == 0 {
var lowestPrio int64
// because of mssql we can not use builder or save xorm syntax, so raw sql it is
if _, err := db.GetEngine(ctx).SQL(`SELECT MAX(priority) FROM protected_branch WHERE repo_id = ?`, protectBranch.RepoID).
Get(&lowestPrio); err != nil {
return err
}
log.Trace("Create new ProtectedBranch at repo[%d] and detect current lowest priority '%d'", protectBranch.RepoID, lowestPrio)
protectBranch.Priority = lowestPrio + 1
}
if _, err = db.GetEngine(ctx).Insert(protectBranch); err != nil {
return fmt.Errorf("Insert: %v", err)
}
return nil
}
// update the rule
if _, err = db.GetEngine(ctx).ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
return fmt.Errorf("Update: %v", err)
}
@ -428,6 +442,24 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
return nil
}
func UpdateProtectBranchPriorities(ctx context.Context, repo *repo_model.Repository, ids []int64) error {
prio := int64(1)
return db.WithTx(ctx, func(ctx context.Context) error {
for _, id := range ids {
if _, err := db.GetEngine(ctx).
ID(id).Where("repo_id = ?", repo.ID).
Cols("priority").
Update(&ProtectedBranch{
Priority: prio,
}); err != nil {
return err
}
prio++
}
return nil
})
}
// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have explicit read or write access to the repo.
func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {

View File

@ -28,6 +28,13 @@ func (rules ProtectedBranchRules) sort() {
sort.Slice(rules, func(i, j int) bool {
rules[i].loadGlob()
rules[j].loadGlob()
// if priority differ, use that to sort
if rules[i].Priority != rules[j].Priority {
return rules[i].Priority < rules[j].Priority
}
// now we sort the old way
if rules[i].isPlainName != rules[j].isPlainName {
return rules[i].isPlainName // plain name comes first, so plain name means "less"
}

View File

@ -75,7 +75,7 @@ func TestBranchRuleMatchPriority(t *testing.T) {
}
}
func TestBranchRuleSort(t *testing.T) {
func TestBranchRuleSortLegacy(t *testing.T) {
in := []*ProtectedBranch{{
RuleName: "b",
CreatedUnix: 1,
@ -103,3 +103,37 @@ func TestBranchRuleSort(t *testing.T) {
}
assert.Equal(t, expect, got)
}
func TestBranchRuleSortPriority(t *testing.T) {
in := []*ProtectedBranch{{
RuleName: "b",
CreatedUnix: 1,
Priority: 4,
}, {
RuleName: "b/*",
CreatedUnix: 3,
Priority: 2,
}, {
RuleName: "a/*",
CreatedUnix: 2,
Priority: 1,
}, {
RuleName: "c",
CreatedUnix: 0,
Priority: 0,
}, {
RuleName: "a",
CreatedUnix: 4,
Priority: 3,
}}
expect := []string{"c", "a/*", "b/*", "a", "b"}
pbr := ProtectedBranchRules(in)
pbr.sort()
var got []string
for i := range pbr {
got = append(got, pbr[i].RuleName)
}
assert.Equal(t, expect, got)
}

View File

@ -7,6 +7,10 @@ import (
"fmt"
"testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
@ -76,3 +80,77 @@ func TestBranchRuleMatch(t *testing.T) {
)
}
}
func TestUpdateProtectBranchPriorities(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
// Create some test protected branches with initial priorities
protectedBranches := []*ProtectedBranch{
{
RepoID: repo.ID,
RuleName: "master",
Priority: 1,
},
{
RepoID: repo.ID,
RuleName: "develop",
Priority: 2,
},
{
RepoID: repo.ID,
RuleName: "feature/*",
Priority: 3,
},
}
for _, pb := range protectedBranches {
_, err := db.GetEngine(db.DefaultContext).Insert(pb)
assert.NoError(t, err)
}
// Test updating priorities
newPriorities := []int64{protectedBranches[2].ID, protectedBranches[0].ID, protectedBranches[1].ID}
err := UpdateProtectBranchPriorities(db.DefaultContext, repo, newPriorities)
assert.NoError(t, err)
// Verify new priorities
pbs, err := FindRepoProtectedBranchRules(db.DefaultContext, repo.ID)
assert.NoError(t, err)
expectedPriorities := map[string]int64{
"feature/*": 1,
"master": 2,
"develop": 3,
}
for _, pb := range pbs {
assert.Equal(t, expectedPriorities[pb.RuleName], pb.Priority)
}
}
func TestNewProtectBranchPriority(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
err := UpdateProtectBranch(db.DefaultContext, repo, &ProtectedBranch{
RepoID: repo.ID,
RuleName: "branch-1",
Priority: 1,
}, WhitelistOptions{})
assert.NoError(t, err)
newPB := &ProtectedBranch{
RepoID: repo.ID,
RuleName: "branch-2",
// Priority intentionally omitted
}
err = UpdateProtectBranch(db.DefaultContext, repo, newPB, WhitelistOptions{})
assert.NoError(t, err)
savedPB2, err := GetFirstMatchProtectedBranchRule(db.DefaultContext, repo.ID, "branch-2")
assert.NoError(t, err)
assert.Equal(t, int64(2), savedPB2.Priority)
}

View File

@ -1108,7 +1108,7 @@ func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList,
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
}
if opts.Page != 0 {
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, opts)
}

View File

@ -7,8 +7,8 @@ import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/renderhelper"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"xorm.io/builder"
@ -112,14 +112,8 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
}
var err error
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Repo: issue.Repo,
Links: markup.Links{
Base: issue.Repo.Link(),
},
Metas: issue.Repo.ComposeMetas(ctx),
}, comment.Content); err != nil {
rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil {
return nil, err
}
}

View File

@ -641,7 +641,7 @@ func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptio
Where("issue_id = ?", issue.ID).
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
if opts.Page != 0 {
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, &opts)
}
err = sess.Find(&issueDeps)

View File

@ -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,

View File

@ -105,7 +105,7 @@ func GetIssueWatchers(ctx context.Context, issueID int64, listOptions db.ListOpt
And("`user`.prohibit_login = ?", false).
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
if listOptions.Page != 0 {
if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
watches := make([]*IssueWatch, 0, listOptions.PageSize)
return watches, sess.Find(&watches)

View File

@ -390,7 +390,7 @@ func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listO
sess.Asc("name")
}
if listOptions.Page != 0 {
if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
}
@ -462,7 +462,7 @@ func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOpt
sess.Asc("name")
}
if listOptions.Page != 0 {
if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
}

View File

@ -406,7 +406,7 @@ func TestDeleteIssueLabel(t *testing.T) {
PosterID: doerID,
IssueID: issueID,
LabelID: labelID,
}, `content=""`)
}, `content=''`)
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
assert.EqualValues(t, expectedNumIssues, label.NumIssues)
assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)

View File

@ -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,

View File

@ -163,7 +163,7 @@ func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList
Where(opts.toConds()).
In("reaction.`type`", setting.UI.Reactions).
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id")
if opts.Page != 0 {
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, &opts)
reactions := make([]*Reaction, 0, opts.PageSize)

View File

@ -96,7 +96,7 @@ func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) {
func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
sws := make([]*Stopwatch, 0, 8)
sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID)
if listOptions.Page != 0 {
if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
}

View File

@ -139,7 +139,7 @@ func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
sess = sess.Where(opts.ToConds())
if opts.Page != 0 {
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, opts)
}

View File

@ -8,7 +8,6 @@ import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"testing"
@ -16,11 +15,10 @@ 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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm"
)
@ -35,35 +33,15 @@ 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)
}
}
require.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)
t.Fatalf("unable to reset database: %v", err)
return nil, deferFn
}
x, err := newXORMEngine()
assert.NoError(t, err)
require.NoError(t, err)
if x != nil {
oldDefer := deferFn
deferFn = func() {
@ -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()

View File

@ -366,7 +366,9 @@ 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 table issue_dev_link", v1_23.CreateTableIssueDevLink),
newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices),
newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch),
newMigration(311, "Add table issue_dev_link", v1_23.CreateTableIssueDevLink),
}
return preparedMigrations
}

View File

@ -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)

View File

@ -7,16 +7,71 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
func CreateTableIssueDevLink(x *xorm.Engine) error {
type IssueDevLink struct {
type improveNotificationTableIndicesAction struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"INDEX"`
LinkType int
LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo
LinkIndex string // branch name, pull request number or commit sha
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
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"`
}
return x.Sync(new(IssueDevLink))
// TableName sets the name of this table
func (*improveNotificationTableIndicesAction) TableName() string {
return "notification"
}
// 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{})
}

View File

@ -0,0 +1,16 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import (
"xorm.io/xorm"
)
func AddPriorityToProtectedBranch(x *xorm.Engine) error {
type ProtectedBranch struct {
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync(new(ProtectedBranch))
}

View File

@ -0,0 +1,22 @@
// 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"
)
func CreateTableIssueDevLink(x *xorm.Engine) error {
type IssueDevLink struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"INDEX"`
LinkType int
LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo
LinkIndex string // branch name, pull request number or commit sha
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
return x.Sync(new(IssueDevLink))
}

View File

@ -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
}

View File

@ -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
Doer *user_model.User
IsDoerMember bool
OrgID int64
PublicOnly bool
}
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)

View File

@ -0,0 +1,163 @@
// 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"
)
type OrgList []*Organization
func (orgs OrgList) LoadTeams(ctx context.Context) (map[int64]TeamList, error) {
if len(orgs) == 0 {
return map[int64]TeamList{}, nil
}
orgIDs := make([]int64, len(orgs))
for i, org := range orgs {
orgIDs[i] = org.ID
}
teams, err := GetTeamsByOrgIDs(ctx, orgIDs)
if err != nil {
return nil, err
}
teamMap := make(map[int64]TeamList, len(orgs))
for _, team := range teams {
teamMap[team.OrgID] = append(teamMap[team.OrgID], team)
}
return teamMap, nil
}
// 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
}

View File

@ -0,0 +1,73 @@
// 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)
}
}
func TestLoadOrgListTeams(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4})
assert.NoError(t, err)
assert.Len(t, orgs, 1)
teamsMap, err := organization.OrgList(orgs).LoadTeams(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, teamsMap, 1)
assert.Len(t, teamsMap[3], 5)
}

View File

@ -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,
})
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, 1, total)
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{},
opts := &organization.FindOrgMembersOpts{
Doer: &user_model.User{IsAdmin: true},
OrgID: 3,
PublicOnly: false,
})
}
assert.False(t, opts.PublicOnly())
orgUsers, err := organization.GetOrgUsersByOrgID(db.DefaultContext, opts)
assert.NoError(t, err)
if assert.Len(t, orgUsers, 3) {
assert.Equal(t, organization.OrgUser{
ID: orgUsers[0].ID,
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,
}, *orgUsers[0])
assert.Equal(t, organization.OrgUser{
ID: orgUsers[1].ID,
}, {
ID: 2,
OrgID: 3,
UID: 4,
IsPublic: false,
}, *orgUsers[1])
assert.Equal(t, organization.OrgUser{
ID: orgUsers[2].ID,
}, {
ID: 9,
OrgID: 3,
UID: 28,
IsPublic: true,
}, *orgUsers[2])
}
}}, 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)

View File

@ -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))
}

View File

@ -126,3 +126,8 @@ func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams T
And("team_repo.repo_id=?", repoID).
Find(&teams)
}
func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) {
teams := make([]*Team, 0, 10)
return teams, db.GetEngine(ctx).Where(builder.In("org_id", orgIDs)).Find(&teams)
}

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"xorm.io/builder"
)
@ -83,3 +84,16 @@ func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode per
OrderBy("name").
Find(&teams)
}
// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit.
func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) {
teams := make([]*Team, 0, 5)
return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode).
Join("INNER", "team_repo", "team_repo.team_id = team.id").
Join("INNER", "team_unit", "team_unit.team_id = team.id").
And("team_repo.org_id = ?", orgID).
And("team_repo.repo_id = ?", repoID).
And("team_unit.type = ?", unitType).
OrderBy("name").
Find(&teams)
}

View File

@ -0,0 +1,31 @@
// 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/perm"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
assert.NoError(t, err)
if assert.Len(t, teams, 2) {
assert.EqualValues(t, 21, teams[0].ID)
assert.EqualValues(t, 22, teams[1].ID)
}
}

View File

@ -197,3 +197,8 @@ func TestUsersInTeamsCount(t *testing.T) {
test([]int64{1, 2, 3, 4, 5}, []int64{2, 5}, 2) // userid 2,4
test([]int64{1, 2, 3, 4, 5}, []int64{2, 3, 5}, 3) // userid 2,4,5
}
func TestIsUsableTeamName(t *testing.T) {
assert.NoError(t, organization.IsUsableTeamName("usable"))
assert.True(t, db.IsErrNameReserved(organization.IsUsableTeamName("new")))
}

View File

@ -60,3 +60,6 @@ func ParseAccessMode(permission string, allowed ...AccessMode) AccessMode {
}
return util.Iif(slices.Contains(allowed, m), m, AccessModeNone)
}
// ErrInvalidAccessMode is returned when an invalid access mode is used
var ErrInvalidAccessMode = util.NewInvalidArgumentErrorf("Invalid access mode")

View File

@ -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",

View File

@ -0,0 +1,53 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"io"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
)
type commitChecker struct {
ctx context.Context
commitCache map[string]bool
gitRepoFacade gitrepo.Repository
gitRepo *git.Repository
gitRepoCloser io.Closer
}
func newCommitChecker(ctx context.Context, gitRepo gitrepo.Repository) *commitChecker {
return &commitChecker{ctx: ctx, commitCache: make(map[string]bool), gitRepoFacade: gitRepo}
}
func (c *commitChecker) Close() error {
if c != nil && c.gitRepoCloser != nil {
return c.gitRepoCloser.Close()
}
return nil
}
func (c *commitChecker) IsCommitIDExisting(commitID string) bool {
exist, inCache := c.commitCache[commitID]
if inCache {
return exist
}
if c.gitRepo == nil {
r, closer, err := gitrepo.RepositoryFromContextOrOpen(c.ctx, c.gitRepoFacade)
if err != nil {
log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(c.gitRepoFacade), err)
return false
}
c.gitRepo, c.gitRepoCloser = r, closer
}
exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
c.commitCache[commitID] = exist
return exist
}

View File

@ -0,0 +1,27 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
FixtureFiles: []string{"repository.yml", "user.yml"},
SetUp: func() error {
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
markup.Init(&markup.RenderHelperFuncs{
IsUsernameMentionable: func(ctx context.Context, username string) bool {
return username == "user2"
},
})
return nil
},
})
}

View File

@ -0,0 +1,73 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"fmt"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/util"
)
type RepoComment struct {
ctx *markup.RenderContext
opts RepoCommentOptions
commitChecker *commitChecker
repoLink string
}
func (r *RepoComment) CleanUp() {
_ = r.commitChecker.Close()
}
func (r *RepoComment) IsCommitIDExisting(commitID string) bool {
return r.commitChecker.IsCommitIDExisting(commitID)
}
func (r *RepoComment) ResolveLink(link string, likeType markup.LinkType) (finalLink string) {
switch likeType {
case markup.LinkTypeApp:
finalLink = r.ctx.ResolveLinkApp(link)
default:
finalLink = r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
}
return finalLink
}
var _ markup.RenderHelper = (*RepoComment)(nil)
type RepoCommentOptions struct {
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
CurrentRefPath string // eg: "branch/main" or "commit/11223344"
}
func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext {
helper := &RepoComment{
repoLink: repo.Link(),
opts: util.OptionalArg(opts),
}
rctx := markup.NewRenderContext(ctx)
helper.ctx = rctx
if repo != nil {
helper.repoLink = repo.Link()
helper.commitChecker = newCommitChecker(ctx, repo)
rctx = rctx.WithMetas(repo.ComposeMetas(ctx))
} else {
// this is almost dead code, only to pass the incorrect tests
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
rctx = rctx.WithMetas(map[string]string{
"user": helper.opts.DeprecatedOwnerName,
"repo": helper.opts.DeprecatedRepoName,
"markdownLineBreakStyle": "comment",
"markupAllowShortIssuePattern": "true",
})
}
rctx = rctx.WithHelper(helper)
return rctx
}

View File

@ -0,0 +1,76 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
)
func TestRepoComment(t *testing.T) {
unittest.PrepareTestEnv(t)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
t.Run("AutoLink", func(t *testing.T) {
rctx := NewRenderContextRepoComment(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow"><code>65f1bf27bc</code></a><br/>
<a href="/user2/repo1/issues/1" class="ref-issue" rel="nofollow">#1</a><br/>
<a href="/user2" rel="nofollow">@user2</a></p>
`, rendered)
})
t.Run("AbsoluteAndRelative", func(t *testing.T) {
rctx := NewRenderContextRepoComment(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
// It is Gitea's old behavior, the relative path is resolved to the repo path
// It is different from GitHub, GitHub resolves relative links to current page's path
rendered, err := markup.RenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/test" rel="nofollow">/test</a><br/>
<a href="/user2/repo1/test" rel="nofollow">./test</a><br/>
<a href="/user2/repo1/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/image" alt="/image"/></a><br/>
<a href="/user2/repo1/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/image" alt="./image"/></a></p>
`, rendered)
})
t.Run("WithCurrentRefPath", func(t *testing.T) {
rctx := NewRenderContextRepoComment(context.Background(), repo1, RepoCommentOptions{CurrentRefPath: "/commit/1234"}).
WithMarkupType(markdown.MarkupName)
// the ref path is only used to render commit message: a commit message is rendered at the commit page with its commit ID path
rendered, err := markup.RenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t, `<p><a href="/user2/repo1/test" rel="nofollow">/test</a><br/>
<a href="/user2/repo1/commit/1234/test" rel="nofollow">./test</a><br/>
<a href="/user2/repo1/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/image" alt="/image"/></a><br/>
<a href="/user2/repo1/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/commit/1234/image" alt="./image"/></a></p>
`, rendered)
})
}

View File

@ -0,0 +1,77 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"fmt"
"path"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/util"
)
type RepoFile struct {
ctx *markup.RenderContext
opts RepoFileOptions
commitChecker *commitChecker
repoLink string
}
func (r *RepoFile) CleanUp() {
_ = r.commitChecker.Close()
}
func (r *RepoFile) IsCommitIDExisting(commitID string) bool {
return r.commitChecker.IsCommitIDExisting(commitID)
}
func (r *RepoFile) ResolveLink(link string, likeType markup.LinkType) string {
finalLink := link
switch likeType {
case markup.LinkTypeApp:
finalLink = r.ctx.ResolveLinkApp(link)
case markup.LinkTypeDefault:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
case markup.LinkTypeRaw:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
case markup.LinkTypeMedia:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
}
return finalLink
}
var _ markup.RenderHelper = (*RepoFile)(nil)
type RepoFileOptions struct {
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
CurrentRefPath string // eg: "branch/main"
CurrentTreePath string // eg: "path/to/file" in the repo
}
func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, opts ...RepoFileOptions) *markup.RenderContext {
helper := &RepoFile{opts: util.OptionalArg(opts)}
rctx := markup.NewRenderContext(ctx)
helper.ctx = rctx
if repo != nil {
helper.repoLink = repo.Link()
helper.commitChecker = newCommitChecker(ctx, repo)
rctx = rctx.WithMetas(repo.ComposeDocumentMetas(ctx))
} else {
// this is almost dead code, only to pass the incorrect tests
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
rctx = rctx.WithMetas(map[string]string{
"user": helper.opts.DeprecatedOwnerName,
"repo": helper.opts.DeprecatedRepoName,
"markdownLineBreakStyle": "document",
})
}
rctx = rctx.WithHelper(helper)
return rctx
}

View File

@ -0,0 +1,83 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
)
func TestRepoFile(t *testing.T) {
unittest.PrepareTestEnv(t)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
t.Run("AutoLink", func(t *testing.T) {
rctx := NewRenderContextRepoFile(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow"><code>65f1bf27bc</code></a>
#1
<a href="/user2" rel="nofollow">@user2</a></p>
`, rendered)
})
t.Run("AbsoluteAndRelative", func(t *testing.T) {
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{CurrentRefPath: "branch/main"}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/src/branch/main/test" rel="nofollow">/test</a>
<a href="/user2/repo1/src/branch/main/test" rel="nofollow">./test</a>
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a>
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p>
`, rendered)
})
t.Run("WithCurrentRefPath", func(t *testing.T) {
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{CurrentRefPath: "/commit/1234"}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
[/test](/test)
![/image](/image)
`)
assert.NoError(t, err)
assert.Equal(t, `<p><a href="/user2/repo1/src/commit/1234/test" rel="nofollow">/test</a>
<a href="/user2/repo1/media/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p>
`, rendered)
})
t.Run("WithCurrentRefPathByTag", func(t *testing.T) {
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{
CurrentRefPath: "/commit/1234",
CurrentTreePath: "my-dir",
}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
<img src="LINK">
<video src="LINK">
`)
assert.NoError(t, err)
assert.Equal(t, `<a href="/user2/repo1/media/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a>
<video src="/user2/repo1/media/commit/1234/my-dir/LINK">
</video>`, rendered)
})
}

View File

@ -0,0 +1,80 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"fmt"
"path"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/util"
)
type RepoWiki struct {
ctx *markup.RenderContext
opts RepoWikiOptions
commitChecker *commitChecker
repoLink string
}
func (r *RepoWiki) CleanUp() {
_ = r.commitChecker.Close()
}
func (r *RepoWiki) IsCommitIDExisting(commitID string) bool {
return r.commitChecker.IsCommitIDExisting(commitID)
}
func (r *RepoWiki) ResolveLink(link string, likeType markup.LinkType) string {
finalLink := link
switch likeType {
case markup.LinkTypeApp:
finalLink = r.ctx.ResolveLinkApp(link)
case markup.LinkTypeDefault:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
case markup.LinkTypeMedia:
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link)
case markup.LinkTypeRaw: // wiki doesn't use it
}
return finalLink
}
var _ markup.RenderHelper = (*RepoWiki)(nil)
type RepoWikiOptions struct {
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
// these options are not used at the moment because Wiki doesn't support sub-path, nor branch
currentRefPath string // eg: "branch/main"
currentTreePath string // eg: "path/to/file" in the repo
}
func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository, opts ...RepoWikiOptions) *markup.RenderContext {
helper := &RepoWiki{opts: util.OptionalArg(opts)}
rctx := markup.NewRenderContext(ctx).WithMarkupType(markdown.MarkupName)
if repo != nil {
helper.repoLink = repo.Link()
helper.commitChecker = newCommitChecker(ctx, repo)
rctx = rctx.WithMetas(repo.ComposeWikiMetas(ctx))
} else {
// this is almost dead code, only to pass the incorrect tests
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
rctx = rctx.WithMetas(map[string]string{
"user": helper.opts.DeprecatedOwnerName,
"repo": helper.opts.DeprecatedRepoName,
"markdownLineBreakStyle": "document",
"markupAllowShortIssuePattern": "true",
})
}
rctx = rctx.WithHelper(helper)
helper.ctx = rctx
return rctx
}

View File

@ -0,0 +1,65 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
)
func TestRepoWiki(t *testing.T) {
unittest.PrepareTestEnv(t)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
t.Run("AutoLink", func(t *testing.T) {
rctx := NewRenderContextRepoWiki(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow"><code>65f1bf27bc</code></a>
<a href="/user2/repo1/issues/1" class="ref-issue" rel="nofollow">#1</a>
<a href="/user2" rel="nofollow">@user2</a></p>
`, rendered)
})
t.Run("AbsoluteAndRelative", func(t *testing.T) {
rctx := NewRenderContextRepoWiki(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t,
`<p><a href="/user2/repo1/wiki/test" rel="nofollow">/test</a>
<a href="/user2/repo1/wiki/test" rel="nofollow">./test</a>
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a>
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p>
`, rendered)
})
t.Run("PathInTag", func(t *testing.T) {
rctx := NewRenderContextRepoWiki(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
<img src="LINK">
<video src="LINK">
`)
assert.NoError(t, err)
assert.Equal(t, `<a href="/user2/repo1/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a>
<video src="/user2/repo1/wiki/raw/LINK">
</video>`, rendered)
})
}

View File

@ -0,0 +1,29 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"code.gitea.io/gitea/modules/markup"
)
type SimpleDocument struct {
*markup.SimpleRenderHelper
ctx *markup.RenderContext
baseLink string
}
func (r *SimpleDocument) ResolveLink(link string, likeType markup.LinkType) string {
return r.ctx.ResolveLinkRelative(r.baseLink, "", link)
}
var _ markup.RenderHelper = (*SimpleDocument)(nil)
func NewRenderContextSimpleDocument(ctx context.Context, baseLink string) *markup.RenderContext {
helper := &SimpleDocument{baseLink: baseLink}
rctx := markup.NewRenderContext(ctx).WithHelper(helper).WithMetas(markup.ComposeSimpleDocumentMetas())
helper.ctx = rctx
return rctx
}

View File

@ -0,0 +1,40 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package renderhelper
import (
"context"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
)
func TestSimpleDocument(t *testing.T) {
unittest.PrepareTestEnv(t)
rctx := NewRenderContextSimpleDocument(context.Background(), "/base").WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
@user2
[/test](/test)
[./test](./test)
![/image](/image)
![./image](./image)
`)
assert.NoError(t, err)
assert.Equal(t,
`<p>65f1bf27bc3bf70f64657658635e66094edbcb4d
#1
<a href="/base/user2" rel="nofollow">@user2</a></p>
<p><a href="/base/test" rel="nofollow">/test</a>
<a href="/base/test" rel="nofollow">./test</a>
<a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="/image"/></a>
<a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="./image"/></a></p>
`, rendered)
}

View File

@ -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)

View File

@ -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,
})
}
mirrors := make([]*PushMirror, 0, 10)
count, err := sess.FindAndCount(&mirrors)
return mirrors, count, err
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
}
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.

View File

@ -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
}

View File

@ -7,6 +7,7 @@ import (
"context"
"fmt"
"html/template"
"maps"
"net"
"net/url"
"path/filepath"
@ -165,8 +166,8 @@ type Repository struct {
Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
RenderingMetas map[string]string `xorm:"-"`
DocumentRenderingMetas map[string]string `xorm:"-"`
commonRenderingMetas map[string]string `xorm:"-"`
Units []*RepoUnit `xorm:"-"`
PrimaryLanguage *LanguageStat `xorm:"-"`
@ -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
@ -606,10 +617,7 @@ func (repo *Repository) CanEnableEditor() bool {
// DescriptionHTML does special handles to description and return HTML string.
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
Ctx: ctx,
// Don't use Metas to speedup requests
}, repo.Description)
desc, err := markup.RenderDescriptionHTML(markup.NewRenderContext(ctx), repo.Description)
if err != nil {
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
return template.HTML(markup.SanitizeDescription(repo.Description))

View File

@ -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

View File

@ -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"))
}

View File

@ -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"],

View File

@ -11,7 +11,6 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
api "code.gitea.io/gitea/modules/structs"
"xorm.io/builder"
)
@ -146,57 +145,6 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
return users, nil
}
// GetReviewers get all users can be requested to review:
// * for private repositories this returns all users that have read access or higher to the repository.
// * for public repositories this returns all users that have read access or higher to the repository,
// all repo watchers and all organization members.
// TODO: may be we should have a busy choice for users to block review request to them.
func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) ([]*user_model.User, error) {
// Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries
if err := repo.LoadOwner(ctx); err != nil {
return nil, err
}
cond := builder.And(builder.Neq{"`user`.id": posterID}).
And(builder.Eq{"`user`.is_active": true})
if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
// This a private repository:
// Anyone who can read the repository is a requestable reviewer
cond = cond.And(builder.In("`user`.id",
builder.Select("user_id").From("access").Where(
builder.Eq{"repo_id": repo.ID}.
And(builder.Gte{"mode": perm.AccessModeRead}),
),
))
if repo.Owner.Type == user_model.UserTypeIndividual && repo.Owner.ID != posterID {
// as private *user* repos don't generate an entry in the `access` table,
// the owner of a private repo needs to be explicitly added.
cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID})
}
} else {
// This is a "public" repository:
// Any user that has read access, is a watcher or organization member can be requested to review
cond = cond.And(builder.And(builder.In("`user`.id",
builder.Select("user_id").From("access").
Where(builder.Eq{"repo_id": repo.ID}.
And(builder.Gte{"mode": perm.AccessModeRead})),
).Or(builder.In("`user`.id",
builder.Select("user_id").From("watch").
Where(builder.Eq{"repo_id": repo.ID}.
And(builder.In("mode", WatchModeNormal, WatchModeAuto))),
).Or(builder.In("`user`.id",
builder.Select("uid").From("org_user").
Where(builder.Eq{"org_id": repo.OwnerID}),
)))))
}
users := make([]*user_model.User, 0, 8)
return users, db.GetEngine(ctx).Where(cond).OrderBy(user_model.GetOrderByName()).Find(&users)
}
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
// If isShowFullName is set to true, also include full name prefix search
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {

View File

@ -38,46 +38,3 @@ func TestRepoAssignees(t *testing.T) {
assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15)
}
}
func TestRepoGetReviewers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// test public repo
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
ctx := db.DefaultContext
reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2)
assert.NoError(t, err)
if assert.Len(t, reviewers, 3) {
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
}
// should include doer if doer is not PR poster.
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2)
assert.NoError(t, err)
assert.Len(t, reviewers, 3)
// should not include PR poster, if PR poster would be otherwise eligible
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4)
assert.NoError(t, err)
assert.Len(t, reviewers, 2)
// test private user repo
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
reviewers, err = repo_model.GetReviewers(ctx, repo2, 2, 4)
assert.NoError(t, err)
assert.Len(t, reviewers, 1)
assert.EqualValues(t, reviewers[0].ID, 2)
// test private org repo
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 1)
assert.NoError(t, err)
assert.Len(t, reviewers, 2)
reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 2)
assert.NoError(t, err)
assert.Len(t, reviewers, 1)
}

View File

@ -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
}

View File

@ -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)
return util.CopyFile(src, dest)
}
// 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
}
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
if src.Size() == dest.Size() &&
src.ModTime() == dest.ModTime() &&
src.Mode() == dest.Mode() {
return nil
}
// Set back file information.
if err = os.Chtimes(dest, si.ModTime(), si.ModTime()); err != nil {
return err
}
return os.Chmod(dest, si.Mode())
return Copy(srcPath, destPath)
}
// 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.
//
// SyncDirs synchronizes files recursively from source to target directory.
// 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)
}
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
}
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)
} else {
err = Copy(path.Join(srcPath, info), curPath)
return err
}
}
}
// 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 = Sync(filepath.Join(srcPath, srcFile), destFilePath)
}
if err != nil {
return err

View File

@ -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
}

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/builder"
)
@ -64,10 +65,10 @@ func BeanExists(t assert.TestingT, bean any, conditions ...any) bool {
}
// AssertExistsAndLoadBean assert that a bean exists and load it from the test database
func AssertExistsAndLoadBean[T any](t assert.TestingT, bean T, conditions ...any) T {
func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T {
exists, err := LoadBeanIfExists(bean, conditions...)
assert.NoError(t, err)
assert.True(t, exists,
require.NoError(t, err)
require.True(t, exists,
"Expected to find %+v (of type %T, with conditions %+v), but did not",
bean, bean, conditions)
return bean

View File

@ -152,7 +152,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
defer sessQuery.Close()
if opts.Page != 0 {
if opts.Page > 0 {
sessQuery = db.SetSessionPagination(sessQuery, opts)
}

View File

@ -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 (
@ -330,7 +330,7 @@ func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListO
And("`user`.type=?", UserTypeIndividual).
And(isUserVisibleToViewerCond(viewer))
if listOptions.Page != 0 {
if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
users := make([]*User, 0, listOptions.PageSize)
@ -352,7 +352,7 @@ func GetUserFollowing(ctx context.Context, u, viewer *User, listOptions db.ListO
And("`user`.type IN (?, ?)", UserTypeIndividual, UserTypeOrganization).
And(isUserVisibleToViewerCond(viewer))
if listOptions.Page != 0 {
if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
users := make([]*User, 0, listOptions.PageSize)
@ -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()})

View File

@ -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)
}

View File

@ -4,13 +4,14 @@
package webauthn
import (
"context"
"encoding/binary"
"encoding/gob"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
@ -38,40 +39,42 @@ func Init() {
}
}
// User represents an implementation of webauthn.User based on User model
type User user_model.User
// user represents an implementation of webauthn.User based on User model
type user struct {
ctx context.Context
User *user_model.User
defaultAuthFlags protocol.AuthenticatorFlags
}
var _ webauthn.User = (*user)(nil)
func NewWebAuthnUser(ctx context.Context, u *user_model.User, defaultAuthFlags ...protocol.AuthenticatorFlags) webauthn.User {
return &user{ctx: ctx, User: u, defaultAuthFlags: util.OptionalArg(defaultAuthFlags)}
}
// WebAuthnID implements the webauthn.User interface
func (u *User) WebAuthnID() []byte {
func (u *user) WebAuthnID() []byte {
id := make([]byte, 8)
binary.PutVarint(id, u.ID)
binary.PutVarint(id, u.User.ID)
return id
}
// WebAuthnName implements the webauthn.User interface
func (u *User) WebAuthnName() string {
if u.LoginName == "" {
return u.Name
}
return u.LoginName
func (u *user) WebAuthnName() string {
return util.IfZero(u.User.LoginName, u.User.Name)
}
// WebAuthnDisplayName implements the webauthn.User interface
func (u *User) WebAuthnDisplayName() string {
return (*user_model.User)(u).DisplayName()
}
// WebAuthnIcon implements the webauthn.User interface
func (u *User) WebAuthnIcon() string {
return (*user_model.User)(u).AvatarLink(db.DefaultContext)
func (u *user) WebAuthnDisplayName() string {
return u.User.DisplayName()
}
// WebAuthnCredentials implements the webauthn.User interface
func (u *User) WebAuthnCredentials() []webauthn.Credential {
dbCreds, err := auth.GetWebAuthnCredentialsByUID(db.DefaultContext, u.ID)
func (u *user) WebAuthnCredentials() []webauthn.Credential {
dbCreds, err := auth.GetWebAuthnCredentialsByUID(u.ctx, u.User.ID)
if err != nil {
return nil
}
return dbCreds.ToCredentials()
return dbCreds.ToCredentials(u.defaultAuthFlags)
}

View File

@ -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

View File

@ -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})

View File

@ -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 {

View File

@ -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"))

View File

@ -7,7 +7,7 @@ import (
"bytes"
stdcsv "encoding/csv"
"io"
"path/filepath"
"path"
"regexp"
"strings"
@ -53,7 +53,7 @@ func CreateReaderAndDetermineDelimiter(ctx *markup.RenderContext, rd io.Reader)
func determineDelimiter(ctx *markup.RenderContext, data []byte) rune {
extension := ".csv"
if ctx != nil {
extension = strings.ToLower(filepath.Ext(ctx.RelativePath))
extension = strings.ToLower(path.Ext(ctx.RenderOptions.RelativePath))
}
var delimiter rune

View File

@ -5,13 +5,13 @@ package csv
import (
"bytes"
"context"
"encoding/csv"
"io"
"strconv"
"strings"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/translation"
@ -231,10 +231,7 @@ John Doe john@doe.com This,note,had,a,lot,of,commas,to,test,delimiters`,
}
for n, c := range cases {
delimiter := determineDelimiter(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: c.filename,
}, []byte(decodeSlashes(t, c.csv)))
delimiter := determineDelimiter(markup.NewRenderContext(context.Background()).WithRelativePath(c.filename), []byte(decodeSlashes(t, c.csv)))
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
}
}

View File

@ -146,9 +146,8 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi
}
// ReadBatchLine reads the header line from cat-file --batch
// We expect:
// <sha> SP <type> SP <size> LF
// sha is a hex encoded here
// We expect: <oid> SP <type> SP <size> LF
// then leaving the rest of the stream "<contents> LF" to be read
func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
typ, err = rd.ReadString('\n')
if err != nil {

View File

@ -9,7 +9,6 @@ import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os/exec"
"strconv"
@ -29,7 +28,7 @@ type Commit struct {
Signature *CommitSignature
Parents []ObjectID // ID strings
submoduleCache *ObjectCache
submoduleCache *ObjectCache[*SubModule]
}
// CommitSignature represents a git commit signature part.
@ -357,69 +356,6 @@ func (c *Commit) GetFileContent(filename string, limit int) (string, error) {
return string(bytes), nil
}
// GetSubModules get all the sub modules of current revision git tree
func (c *Commit) GetSubModules() (*ObjectCache, error) {
if c.submoduleCache != nil {
return c.submoduleCache, nil
}
entry, err := c.GetTreeEntryByPath(".gitmodules")
if err != nil {
if _, ok := err.(ErrNotExist); ok {
return nil, nil
}
return nil, err
}
rd, err := entry.Blob().DataAsync()
if err != nil {
return nil, err
}
defer rd.Close()
scanner := bufio.NewScanner(rd)
c.submoduleCache = newObjectCache()
var ismodule bool
var path string
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "[submodule") {
ismodule = true
continue
}
if ismodule {
fields := strings.Split(scanner.Text(), "=")
k := strings.TrimSpace(fields[0])
if k == "path" {
path = strings.TrimSpace(fields[1])
} else if k == "url" {
c.submoduleCache.Set(path, &SubModule{path, strings.TrimSpace(fields[1])})
ismodule = false
}
}
}
if err = scanner.Err(); err != nil {
return nil, fmt.Errorf("GetSubModules scan: %w", err)
}
return c.submoduleCache, nil
}
// GetSubModule get the sub module according entryname
func (c *Commit) GetSubModule(entryname string) (*SubModule, error) {
modules, err := c.GetSubModules()
if err != nil {
return nil, err
}
if modules != nil {
module, has := modules.Get(entryname)
if has {
return module.(*SubModule), nil
}
}
return nil, nil
}
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
func (c *Commit) GetBranchName() (string, error) {
cmd := NewCommand(c.repo.Ctx, "name-rev")

Some files were not shown because too many files have changed in this diff Show More