1
1
mirror of https://github.com/go-gitea/gitea synced 2025-01-15 20:24:27 +00:00

Merge branch 'main' into api-repo-actions

This commit is contained in:
Chester 2023-09-11 14:24:40 -04:00 committed by GitHub
commit 5ababc57bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
221 changed files with 4028 additions and 2900 deletions

View File

@ -154,7 +154,7 @@ steps:
when: when:
event: event:
exclude: exclude:
- pull_request - pull_request
- name: publish-rootless - name: publish-rootless
image: plugins/docker:latest image: plugins/docker:latest
@ -176,7 +176,7 @@ steps:
when: when:
event: event:
exclude: exclude:
- pull_request - pull_request
--- ---
kind: pipeline kind: pipeline
@ -220,7 +220,7 @@ steps:
when: when:
event: event:
exclude: exclude:
- pull_request - pull_request
- name: publish-rootless - name: publish-rootless
image: plugins/docker:latest image: plugins/docker:latest
@ -241,7 +241,7 @@ steps:
when: when:
event: event:
exclude: exclude:
- pull_request - pull_request
--- ---
kind: pipeline kind: pipeline
@ -289,7 +289,7 @@ steps:
when: when:
event: event:
exclude: exclude:
- pull_request - pull_request
- name: publish-rootless - name: publish-rootless
image: plugins/docker:latest image: plugins/docker:latest
@ -311,7 +311,7 @@ steps:
when: when:
event: event:
exclude: exclude:
- pull_request - pull_request
--- ---
kind: pipeline kind: pipeline
@ -355,7 +355,7 @@ steps:
when: when:
event: event:
exclude: exclude:
- pull_request - pull_request
- name: publish-rootless - name: publish-rootless
image: plugins/docker:latest image: plugins/docker:latest
@ -376,7 +376,7 @@ steps:
when: when:
event: event:
exclude: exclude:
- pull_request - pull_request
--- ---
kind: pipeline kind: pipeline
@ -413,7 +413,7 @@ steps:
trigger: trigger:
ref: ref:
- "refs/tags/**" - "refs/tags/**"
paths: paths:
exclude: exclude:
- "docs/**" - "docs/**"

View File

@ -156,7 +156,7 @@ rules:
import/no-restricted-paths: [0] import/no-restricted-paths: [0]
import/no-self-import: [2] import/no-self-import: [2]
import/no-unassigned-import: [0] import/no-unassigned-import: [0]
import/no-unresolved: [2, {commonjs: true, ignore: [\?.+$, ^vitest/]}] import/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$", ^vitest/]}]
import/no-unused-modules: [2, {unusedExports: true}] import/no-unused-modules: [2, {unusedExports: true}]
import/no-useless-path-segments: [2, {commonjs: true}] import/no-useless-path-segments: [2, {commonjs: true}]
import/no-webpack-loader-syntax: [2] import/no-webpack-loader-syntax: [2]

View File

@ -2,90 +2,90 @@ name: Bug Report
description: Found something you weren't expecting? Report it here! description: Found something you weren't expecting? Report it here!
labels: ["kind/bug"] labels: ["kind/bug"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue. NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
1. Please speak English, this is the language all maintainers can speak and write. 1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord 2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). server (https://discord.gg/gitea) or forum (https://discourse.gitea.io).
3. Make sure you are using the latest release and 3. Make sure you are using the latest release and
take a moment to check that your issue hasn't been reported before. take a moment to check that your issue hasn't been reported before.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq) 4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. It's really important to provide pertinent details and logs (https://docs.gitea.com/help/support), 5. It's really important to provide pertinent details and logs (https://docs.gitea.com/help/support),
incomplete details will be handled as an invalid report. incomplete details will be handled as an invalid report.
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
label: Description label: Description
description: | description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below) Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
If you are using a proxy or a CDN (e.g. Cloudflare) in front of Gitea, please disable the proxy/CDN fully and access Gitea directly to confirm the issue still persists without those services. If you are using a proxy or a CDN (e.g. Cloudflare) in front of Gitea, please disable the proxy/CDN fully and access Gitea directly to confirm the issue still persists without those services.
- type: input - type: input
id: gitea-ver id: gitea-ver
attributes: attributes:
label: Gitea Version label: Gitea Version
description: Gitea version (or commit reference) of your instance description: Gitea version (or commit reference) of your instance
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
id: can-reproduce id: can-reproduce
attributes: attributes:
label: Can you reproduce the bug on the Gitea demo site? label: Can you reproduce the bug on the Gitea demo site?
description: | description: |
If so, please provide a URL in the Description field If so, please provide a URL in the Description field
URL of Gitea demo: https://try.gitea.io URL of Gitea demo: https://try.gitea.io
options: options:
- "Yes" - "Yes"
- "No" - "No"
validations: validations:
required: true required: true
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
It's really important to provide pertinent logs It's really important to provide pertinent logs
Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help
In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini
- type: input - type: input
id: logs id: logs
attributes: attributes:
label: Log Gist label: Log Gist
description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden
- type: textarea - type: textarea
id: screenshots id: screenshots
attributes: attributes:
label: Screenshots label: Screenshots
description: If this issue involves the Web Interface, please provide one or more screenshots description: If this issue involves the Web Interface, please provide one or more screenshots
- type: input - type: input
id: git-ver id: git-ver
attributes: attributes:
label: Git Version label: Git Version
description: The version of git running on the server description: The version of git running on the server
- type: input - type: input
id: os-ver id: os-ver
attributes: attributes:
label: Operating System label: Operating System
description: The operating system you are using to run Gitea description: The operating system you are using to run Gitea
- type: textarea - type: textarea
id: run-info id: run-info
attributes: attributes:
label: How are you running Gitea? label: How are you running Gitea?
description: | description: |
Please include information on whether you built Gitea yourself, used one of our downloads, are using https://try.gitea.io or are using some other package Please include information on whether you built Gitea yourself, used one of our downloads, are using https://try.gitea.io or are using some other package
Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc. Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc.
If you are using a package or systemd tell us what distribution you are using If you are using a package or systemd tell us what distribution you are using
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
id: database id: database
attributes: attributes:
label: Database label: Database
description: What database system are you running? description: What database system are you running?
options: options:
- PostgreSQL - PostgreSQL
- MySQL/MariaDB - MySQL/MariaDB
- MSSQL - MSSQL
- SQLite - SQLite

View File

@ -2,23 +2,23 @@ name: Feature Request
description: Got an idea for a feature that Gitea doesn't have currently? Submit your idea here! description: Got an idea for a feature that Gitea doesn't have currently? Submit your idea here!
labels: ["kind/proposal"] labels: ["kind/proposal"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
1. Please speak English, this is the language all maintainers can speak and write. 1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord 2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). server (https://discord.gg/gitea) or forum (https://discourse.gitea.io).
3. Please take a moment to check that your feature hasn't already been suggested. 3. Please take a moment to check that your feature hasn't already been suggested.
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
label: Feature Description label: Feature Description
placeholder: | placeholder: |
I think it would be great if Gitea had... I think it would be great if Gitea had...
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: screenshots id: screenshots
attributes: attributes:
label: Screenshots label: Screenshots
description: If you can, provide screenshots of an implementation on another site e.g. GitHub description: If you can, provide screenshots of an implementation on another site e.g. GitHub

View File

@ -2,65 +2,65 @@ name: Web Interface Bug Report
description: Something doesn't look quite as it should? Report it here! description: Something doesn't look quite as it should? Report it here!
labels: ["kind/bug", "kind/ui"] labels: ["kind/bug", "kind/ui"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue. NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
1. Please speak English, this is the language all maintainers can speak and write. 1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord 2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). server (https://discord.gg/gitea) or forum (https://discourse.gitea.io).
3. Please take a moment to check that your issue doesn't already exist. 3. Please take a moment to check that your issue doesn't already exist.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq) 4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. Please give all relevant information below for bug reports, because 5. Please give all relevant information below for bug reports, because
incomplete details will be handled as an invalid report. incomplete details will be handled as an invalid report.
6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript 6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript
error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us
DEBUG level logs. (See https://docs.gitea.com/administration/logging-config#collecting-logs-for-help) DEBUG level logs. (See https://docs.gitea.com/administration/logging-config#collecting-logs-for-help)
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
label: Description label: Description
description: | description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below) Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please disable the proxy/CDN fully and connect to gitea directly to confirm the issue still persists without those services. If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please disable the proxy/CDN fully and connect to gitea directly to confirm the issue still persists without those services.
- type: textarea - type: textarea
id: screenshots id: screenshots
attributes: attributes:
label: Screenshots label: Screenshots
description: Please provide at least 1 screenshot showing the issue. description: Please provide at least 1 screenshot showing the issue.
validations: validations:
required: true required: true
- type: input - type: input
id: gitea-ver id: gitea-ver
attributes: attributes:
label: Gitea Version label: Gitea Version
description: Gitea version (or commit reference) your instance is running description: Gitea version (or commit reference) your instance is running
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
id: can-reproduce id: can-reproduce
attributes: attributes:
label: Can you reproduce the bug on the Gitea demo site? label: Can you reproduce the bug on the Gitea demo site?
description: | description: |
If so, please provide a URL in the Description field If so, please provide a URL in the Description field
URL of Gitea demo: https://try.gitea.io URL of Gitea demo: https://try.gitea.io
options: options:
- "Yes" - "Yes"
- "No" - "No"
validations: validations:
required: true required: true
- type: input - type: input
id: os-ver id: os-ver
attributes: attributes:
label: Operating System label: Operating System
description: The operating system you are using to access Gitea description: The operating system you are using to access Gitea
- type: input - type: input
id: browser-ver id: browser-ver
attributes: attributes:
label: Browser Version label: Browser Version
description: The browser and version that you are using to access Gitea description: The browser and version that you are using to access Gitea
validations: validations:
required: true required: true

32
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,32 @@
kind/docs:
- "**/*.md"
- "docs/**"
kind/ui:
- "web_src/**/*"
- all: ["templates/**", "!templates/swagger/v1_json.tmpl"]
kind/api:
- "templates/swagger/v1_json.tmpl"
- "routers/api/**"
kind/build:
- "Makefile"
- "Dockerfile"
- "Dockerfile.rootless"
- "docker/**"
- "webpack.config.js"
theme/package-registry:
- "modules/packages/**"
kind/cli:
- "cmd/**"
kind/lint:
- ".eslintrc.yaml"
- ".golangci.yml"
- ".markdownlint.yaml"
- ".spectral.yaml"
- ".stylelintrc.yaml"
- ".yamllint.yaml"

View File

@ -17,6 +17,8 @@ on:
value: ${{ jobs.detect.outputs.docker }} value: ${{ jobs.detect.outputs.docker }}
swagger: swagger:
value: ${{ jobs.detect.outputs.swagger }} value: ${{ jobs.detect.outputs.swagger }}
yaml:
value: ${{ jobs.detect.outputs.yaml }}
jobs: jobs:
detect: detect:
@ -30,6 +32,7 @@ jobs:
templates: ${{ steps.changes.outputs.templates }} templates: ${{ steps.changes.outputs.templates }}
docker: ${{ steps.changes.outputs.docker }} docker: ${{ steps.changes.outputs.docker }}
swagger: ${{ steps.changes.outputs.swagger }} swagger: ${{ steps.changes.outputs.swagger }}
yaml: ${{ steps.changes.outputs.yaml }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: dorny/paths-filter@v2 - uses: dorny/paths-filter@v2
@ -82,3 +85,8 @@ jobs:
- "package.json" - "package.json"
- "package-lock.json" - "package-lock.json"
- ".spectral.yaml" - ".spectral.yaml"
yaml:
- "**/*.yml"
- "**/*.yaml"
- ".yamllint.yaml"

View File

@ -39,6 +39,19 @@ jobs:
- run: make deps-py - run: make deps-py
- run: make lint-templates - run: make lint-templates
lint-yaml:
if: needs.files-changed.outputs.yaml == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- run: pip install poetry
- run: make deps-py
- run: make lint-yaml
lint-swagger: lint-swagger:
if: needs.files-changed.outputs.swagger == 'true' if: needs.files-changed.outputs.swagger == 'true'
needs: files-changed needs: files-changed

View File

@ -88,7 +88,7 @@ jobs:
mysql: mysql:
image: mysql:5.7 image: mysql:5.7
env: env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: test MYSQL_DATABASE: test
ports: ports:
- "3306:3306" - "3306:3306"
@ -160,7 +160,7 @@ jobs:
mysql: mysql:
image: mysql:5.7 image: mysql:5.7
env: env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: test MYSQL_DATABASE: test
ports: ports:
- "3306:3306" - "3306:3306"
@ -205,7 +205,7 @@ jobs:
mysql8: mysql8:
image: mysql:8 image: mysql:8
env: env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: testgitea MYSQL_DATABASE: testgitea
ports: ports:
- "3306:3306" - "3306:3306"

21
.github/workflows/pull-labeler.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: labeler
on:
pull_request_target:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@v4
with:
dot: true
sync-labels: true

48
.yamllint.yaml Normal file
View File

@ -0,0 +1,48 @@
extends: default
rules:
braces:
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: 0
max-spaces-inside-empty: 0
brackets:
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: 0
max-spaces-inside-empty: 0
comments:
require-starting-space: true
ignore-shebangs: true
min-spaces-from-content: 1
comments-indentation:
level: error
document-start:
level: error
present: false
ignore: |
/.drone.yml
document-end:
present: false
empty-lines:
max: 1
indentation:
spaces: 2
line-length: disable
truthy:
allowed-values: ["true", "false", "on", "off"]
ignore: |
.venv
node_modules
/models/fixtures
/models/migrations/fixtures

View File

@ -4,6 +4,34 @@ This changelog goes through all the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com). been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08
* SECURITY
* Check blocklist for emails when adding them to account (#26812) (#26831)
* ENHANCEMENTS
* Add `branch_filter` to hooks API endpoints (#26599) (#26632)
* Fix incorrect "tabindex" attributes (#26733) (#26734)
* Use line-height: normal by default (#26635) (#26708)
* Fix unable to display individual-level project (#26198) (#26636)
* BUGFIXES
* Fix wrong review requested number (#26784) (#26880)
* Avoid double-unescaping of form value (#26853) (#26863)
* Redirect from `{repo}/issues/new` to `{repo}/issues/new/choose` when blank issues are disabled (#26813) (#26847)
* Sync tags when adopting repos (#26816) (#26834)
* Fix verifyCommits error when push a new branch (#26664) (#26810)
* Include the GITHUB_TOKEN/GITEA_TOKEN secret for fork pull requests (#26759) (#26806)
* Fix some slice append usages (#26778) (#26798)
* Add fix incorrect can_create_org_repo for org owner team (#26683) (#26791)
* Fix bug for ctx usage (#26763)
* Make issue template field template access correct template data (#26698) (#26709)
* Use correct minio error (#26634) (#26639)
* Ignore the trailing slashes when comparing oauth2 redirect_uri (#26597) (#26618)
* Set errwriter for urfave/cli v1 (#26616)
* Fix reopen logic for agit flow pull request (#26399) (#26613)
* Fix context filter has no effect in dashboard (#26695) (#26811)
* Fix being unable to use a repo that prohibits accepting PRs as a PR source. (#26785) (#26790)
* Fix Page Not Found error (#26768)
## [1.20.3](https://github.com/go-gitea/gitea/releases/tag/v1.20.3) - 2023-08-20 ## [1.20.3](https://github.com/go-gitea/gitea/releases/tag/v1.20.3) - 2023-08-20
* BREAKING * BREAKING

View File

@ -218,6 +218,7 @@ help:
@echo " - lint-md lint markdown files" @echo " - lint-md lint markdown files"
@echo " - lint-swagger lint swagger files" @echo " - lint-swagger lint swagger files"
@echo " - lint-templates lint template files" @echo " - lint-templates lint template files"
@echo " - lint-yaml lint yaml files"
@echo " - checks run various consistency checks" @echo " - checks run various consistency checks"
@echo " - checks-frontend check frontend files" @echo " - checks-frontend check frontend files"
@echo " - checks-backend check backend files" @echo " - checks-backend check backend files"
@ -427,6 +428,10 @@ lint-actions:
lint-templates: .venv lint-templates: .venv
@poetry run djlint $(shell find templates -type f -iname '*.tmpl') @poetry run djlint $(shell find templates -type f -iname '*.tmpl')
.PHONY: lint-yaml
lint-yaml: .venv
@poetry run yamllint .
.PHONY: watch .PHONY: watch
watch: watch:
@bash build/watch.sh @bash build/watch.sh

View File

@ -185,7 +185,7 @@ func runMigrateStorage(ctx *cli.Context) error {
case string(setting.LocalStorageType): case string(setting.LocalStorageType):
p := ctx.String("path") p := ctx.String("path")
if p == "" { if p == "" {
log.Fatal("Path must be given when storage is loal") log.Fatal("Path must be given when storage is local")
return nil return nil
} }
dstStorage, err = storage.NewLocalStorage( dstStorage, err = storage.NewLocalStorage(

View File

@ -759,6 +759,8 @@ LEVEL = Info
;; ;;
;; More detail: https://github.com/gogits/gogs/issues/165 ;; More detail: https://github.com/gogits/gogs/issues/165
;ENABLE_REVERSE_PROXY_AUTHENTICATION = false ;ENABLE_REVERSE_PROXY_AUTHENTICATION = false
; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible.
;ENABLE_REVERSE_PROXY_AUTHENTICATION_API = false
;ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false ;ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
;ENABLE_REVERSE_PROXY_EMAIL = false ;ENABLE_REVERSE_PROXY_EMAIL = false
;ENABLE_REVERSE_PROXY_FULL_NAME = false ;ENABLE_REVERSE_PROXY_FULL_NAME = false
@ -1744,8 +1746,8 @@ LEVEL = Info
;; Session cookie name ;; Session cookie name
;COOKIE_NAME = i_like_gitea ;COOKIE_NAME = i_like_gitea
;; ;;
;; If you use session in https only, default is false ;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
;COOKIE_SECURE = false ;COOKIE_SECURE =
;; ;;
;; Session GC time interval in seconds, default is 86400 (1 day) ;; Session GC time interval in seconds, default is 86400 (1 day)
;GC_INTERVAL_TIME = 86400 ;GC_INTERVAL_TIME = 86400
@ -2564,6 +2566,8 @@ LEVEL = Info
;; ;;
;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. ;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance.
;DEFAULT_ACTIONS_URL = github ;DEFAULT_ACTIONS_URL = github
;; Default artifact retention time in days, default is 90 days
;ARTIFACT_RETENTION_DAYS = 90
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -446,7 +446,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `SQLITE_JOURNAL_MODE`: **""**: Change journal mode for SQlite3. Can be used to enable [WAL mode](https://www.sqlite.org/wal.html) when high load causes write congestion. See [SQlite3 docs](https://www.sqlite.org/pragma.html#pragma_journal_mode) for possible values. Defaults to the default for the database file, often DELETE. - `SQLITE_JOURNAL_MODE`: **""**: Change journal mode for SQlite3. Can be used to enable [WAL mode](https://www.sqlite.org/wal.html) when high load causes write congestion. See [SQlite3 docs](https://www.sqlite.org/pragma.html#pragma_journal_mode) for possible values. Defaults to the default for the database file, often DELETE.
- `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating. - `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating.
- `PATH`: **data/gitea.db**: For SQLite3 only, the database file path. - `PATH`: **data/gitea.db**: For SQLite3 only, the database file path.
- `LOG_SQL`: **true**: Log the executed SQL. - `LOG_SQL`: **false**: Log the executed SQL.
- `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed. - `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed.
- `DB_RETRY_BACKOFF`: **3s**: time.Duration to wait before trying another ORM init / DB connect attempt, if failure occurred. - `DB_RETRY_BACKOFF`: **3s**: time.Duration to wait before trying another ORM init / DB connect attempt, if failure occurred.
- `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit. - `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit.
@ -621,7 +621,8 @@ And the following unique queues:
BASIC and the user's password. Please note if you disable this you will not be able to access the BASIC and the user's password. Please note if you disable this you will not be able to access the
tokens API endpoints using a password. Further, this only disables BASIC authentication using the tokens API endpoints using a password. Further, this only disables BASIC authentication using the
password - not tokens or OAuth Basic. password - not tokens or OAuth Basic.
- `ENABLE_REVERSE_PROXY_AUTHENTICATION`: **false**: Enable this to allow reverse proxy authentication. - `ENABLE_REVERSE_PROXY_AUTHENTICATION`: **false**: Enable this to allow reverse proxy authentication for web requests
- `ENABLE_REVERSE_PROXY_AUTHENTICATION_API`: **false**: Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible.
- `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: **false**: Enable this to allow auto-registration - `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: **false**: Enable this to allow auto-registration
for reverse authentication. for reverse authentication.
- `ENABLE_REVERSE_PROXY_EMAIL`: **false**: Enable this to allow to auto-registration with a - `ENABLE_REVERSE_PROXY_EMAIL`: **false**: Enable this to allow to auto-registration with a
@ -776,7 +777,7 @@ and
- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]` - `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]`
- `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_. - `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_.
- `COOKIE_SECURE`: **false**: Enable this to force using HTTPS for all session access. - `COOKIE_SECURE`:**_empty_**: `true` or `false`. Enable this to force using HTTPS for all session access. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
- `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID. - `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID.
- `GC_INTERVAL_TIME`: **86400**: GC interval in seconds. - `GC_INTERVAL_TIME`: **86400**: GC interval in seconds.
- `SESSION_LIFE_TIME`: **86400**: Session life time in seconds, default is 86400 (1 day) - `SESSION_LIFE_TIME`: **86400**: Session life time in seconds, default is 86400 (1 day)
@ -955,6 +956,12 @@ Default templates for project boards:
- `SCHEDULE`: **@midnight** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts. - `SCHEDULE`: **@midnight** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts.
- `UPDATE_EXISTING`: **true**: Create new users, update existing user data and disable users that are not in external source anymore (default) or only create new users if UPDATE_EXISTING is set to false. - `UPDATE_EXISTING`: **true**: Create new users, update existing user data and disable users that are not in external source anymore (default) or only create new users if UPDATE_EXISTING is set to false.
## Cron - Cleanup Expired Actions Assets (`cron.cleanup_actions`)
- `ENABLED`: **true**: Enable cleanup expired actions assets job.
- `RUN_AT_START`: **true**: Run job at start time (if ENABLED).
- `SCHEDULE`: **@midnight** : Cron syntax for the job.
### Extended cron tasks (not enabled by default) ### Extended cron tasks (not enabled by default)
#### Cron - Garbage collect all repositories (`cron.git_gc_repos`) #### Cron - Garbage collect all repositories (`cron.git_gc_repos`)
@ -1381,6 +1388,7 @@ PROXY_HOSTS = *.github.com
- `DEFAULT_ACTIONS_URL`: **github**: Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. - `DEFAULT_ACTIONS_URL`: **github**: Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance.
- `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]` - `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`
- `MINIO_BASE_PATH`: **actions_log/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio` - `MINIO_BASE_PATH`: **actions_log/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio`
- `ARTIFACT_RETENTION_DAYS`: **90**: Number of days to keep artifacts. Set to 0 to disable artifact retention. Default is 90 days if not set.
`DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path. `DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path.
For example, `uses: actions/checkout@v3` means `https://github.com/actions/checkout@v3` since the value of `DEFAULT_ACTIONS_URL` is `github`. For example, `uses: actions/checkout@v3` means `https://github.com/actions/checkout@v3` since the value of `DEFAULT_ACTIONS_URL` is `github`.

View File

@ -436,7 +436,7 @@ menu:
- `SQLITE_JOURNAL_MODE`**""**:更改 SQlite3 的日志模式。可以用于在高负载导致写入拥塞时启用 [WAL 模式](https://www.sqlite.org/wal.html)。有关可能的值,请参阅 [SQlite3 文档](https://www.sqlite.org/pragma.html#pragma_journal_mode)。默认为数据库文件的默认值,通常为 DELETE。 - `SQLITE_JOURNAL_MODE`**""**:更改 SQlite3 的日志模式。可以用于在高负载导致写入拥塞时启用 [WAL 模式](https://www.sqlite.org/wal.html)。有关可能的值,请参阅 [SQlite3 文档](https://www.sqlite.org/pragma.html#pragma_journal_mode)。默认为数据库文件的默认值,通常为 DELETE。
- `ITERATE_BUFFER_SIZE`**50**:用于迭代的内部缓冲区大小。 - `ITERATE_BUFFER_SIZE`**50**:用于迭代的内部缓冲区大小。
- `PATH`**data/gitea.db**:仅适用于 SQLite3 的数据库文件路径。 - `PATH`**data/gitea.db**:仅适用于 SQLite3 的数据库文件路径。
- `LOG_SQL`**true**:记录已执行的 SQL。 - `LOG_SQL`**false**:记录已执行的 SQL。
- `DB_RETRIES`**10**:允许多少次 ORM 初始化 / DB 连接尝试。 - `DB_RETRIES`**10**:允许多少次 ORM 初始化 / DB 连接尝试。
- `DB_RETRY_BACKOFF`**3s**:如果发生故障,等待另一个 ORM 初始化 / DB 连接尝试的 time.Duration。 - `DB_RETRY_BACKOFF`**3s**:如果发生故障,等待另一个 ORM 初始化 / DB 连接尝试的 time.Duration。
- `MAX_OPEN_CONNS`**0**:数据库最大打开连接数 - 默认为 0表示没有限制。 - `MAX_OPEN_CONNS`**0**:数据库最大打开连接数 - 默认为 0表示没有限制。
@ -742,7 +742,7 @@ Gitea 创建以下非唯一队列:
- `PROVIDER`: **memory**:会话存储引擎 \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]。设置为 `db` 将会重用 `[database]` 的配置信息。 - `PROVIDER`: **memory**:会话存储引擎 \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]。设置为 `db` 将会重用 `[database]` 的配置信息。
- `PROVIDER_CONFIG`: **data/sessions**:对于文件,为根路径;对于 db为空将使用数据库配置对于其他引擎为连接字符串。相对路径将根据 _`AppWorkPath`_ 绝对化。 - `PROVIDER_CONFIG`: **data/sessions**:对于文件,为根路径;对于 db为空将使用数据库配置对于其他引擎为连接字符串。相对路径将根据 _`AppWorkPath`_ 绝对化。
- `COOKIE_SECURE`: **false**启用此选项以强制在所有会话访问中使用 HTTPS。 - `COOKIE_SECURE`: **_empty_**`true``false`启用此选项以强制在所有会话访问中使用 HTTPS。如果没有设置,当 ROOT_URL 是 https 链接的时候默认设置为 true。
- `COOKIE_NAME`: **i\_like\_gitea**:用于会话 ID 的 cookie 名称。 - `COOKIE_NAME`: **i\_like\_gitea**:用于会话 ID 的 cookie 名称。
- `GC_INTERVAL_TIME`: **86400**GC 间隔时间,以秒为单位。 - `GC_INTERVAL_TIME`: **86400**GC 间隔时间,以秒为单位。
- `SESSION_LIFE_TIME`: **86400**:会话生命周期,以秒为单位,默认为 864001 天)。 - `SESSION_LIFE_TIME`: **86400**:会话生命周期,以秒为单位,默认为 864001 天)。

View File

@ -29,6 +29,8 @@ server {
location / { location / {
client_max_body_size 512M; client_max_body_size 512M;
proxy_pass http://localhost:3000; proxy_pass http://localhost:3000;
proxy_set_header Connection $http_connection;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@ -92,6 +92,12 @@ it's recommended to use `const _promise = asyncFoo()` to tell readers
that this is done by purpose, we want to call the async function and ignore the Promise. that this is done by purpose, we want to call the async function and ignore the Promise.
Some lint rules and IDEs also have warnings if the returned Promise is not handled. Some lint rules and IDEs also have warnings if the returned Promise is not handled.
### Fetching data
To fetch data, use the wrapper functions `GET`, `POST` etc. from `modules/fetch.js`. They
accept a `data` option for the content, will automatically set CSFR token and return a
Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
### HTML Attributes and `dataset` ### HTML Attributes and `dataset`
The usage of `dataset` is forbidden, its camel-casing behaviour makes it hard to grep for attributes. The usage of `dataset` is forbidden, its camel-casing behaviour makes it hard to grep for attributes.

View File

@ -51,6 +51,15 @@ Open "Windows Services", search for the service named "gitea", right-click it an
"Run". If everything is OK, Gitea will be reachable on `http://localhost:3000` (or the port "Run". If everything is OK, Gitea will be reachable on `http://localhost:3000` (or the port
that was configured). that was configured).
## Service startup type
It was observed that on loaded systems during boot Gitea service may fail to start with timeout records in Windows Event Log.
In that case change startup type to `Automatic-Delayed`. This can be done during service creation, or by running config command
```
sc.exe config gitea start= delayed-auto
```
## Adding startup dependencies ## Adding startup dependencies
To add a startup dependency to the Gitea Windows service (eg Mysql, Mariadb), as an Administrator, then run the following command: To add a startup dependency to the Gitea Windows service (eg Mysql, Mariadb), as an Administrator, then run the following command:

View File

@ -15,6 +15,6 @@ menu:
# Profile READMEs # Profile READMEs
To display a markdown file in your Gitea profile page, simply make a repository named ".profile" and edit the README.md file inside. Gitea will automatically pull this file in and display it above your repositories. To display a Markdown file in your Gitea profile page, simply create a repository named `.profile` and add a new file called `README.md`. Gitea will automatically display the contents of the file on your profile, above your repositories.
Note. You are welcome to make this repository private. Doing so will hide your source files from public viewing and allow you to privitize certain files. However, the README.md file will be the only file present on your profile. If you wish to have an entirely private .profile repository, remove or rename the README.md file. Making the `.profile` repository private will hide the Profile README.

2
go.mod
View File

@ -17,7 +17,7 @@ require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1
github.com/alecthomas/chroma/v2 v2.8.0 github.com/alecthomas/chroma/v2 v2.9.1
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.3.9 github.com/blevesearch/bleve/v2 v2.3.9
github.com/bufbuild/connect-go v1.10.0 github.com/bufbuild/connect-go v1.10.0

4
go.sum
View File

@ -119,8 +119,8 @@ github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= github.com/alecthomas/chroma/v2 v2.9.1 h1:0O3lTQh9FxazJ4BYE/MOi/vDGuHn7B+6Bu902N2UZvU=
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= github.com/alecthomas/chroma/v2 v2.9.1/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=

View File

@ -9,19 +9,21 @@ package actions
import ( import (
"context" "context"
"errors" "errors"
"time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
// ArtifactStatus is the status of an artifact, uploading, expired or need-delete
type ArtifactStatus int64
const ( const (
// ArtifactStatusUploadPending is the status of an artifact upload that is pending ArtifactStatusUploadPending ArtifactStatus = iota + 1 // 1 ArtifactStatusUploadPending is the status of an artifact upload that is pending
ArtifactStatusUploadPending = 1 ArtifactStatusUploadConfirmed // 2 ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed
// ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed ArtifactStatusUploadError // 3 ArtifactStatusUploadError is the status of an artifact upload that is errored
ArtifactStatusUploadConfirmed = 2 ArtifactStatusExpired // 4, ArtifactStatusExpired is the status of an artifact that is expired
// ArtifactStatusUploadError is the status of an artifact upload that is errored
ArtifactStatusUploadError = 3
) )
func init() { func init() {
@ -45,9 +47,10 @@ type ActionArtifact struct {
Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
CreatedUnix timeutil.TimeStamp `xorm:"created"` CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
} }
func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string) (*ActionArtifact, error) { func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) {
if err := t.LoadJob(ctx); err != nil { if err := t.LoadJob(ctx); err != nil {
return nil, err return nil, err
} }
@ -61,7 +64,8 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
RepoID: t.RepoID, RepoID: t.RepoID,
OwnerID: t.OwnerID, OwnerID: t.OwnerID,
CommitSHA: t.CommitSHA, CommitSHA: t.CommitSHA,
Status: ArtifactStatusUploadPending, Status: int64(ArtifactStatusUploadPending),
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + 3600*24*expiredDays),
} }
if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { if _, err := db.GetEngine(ctx).Insert(artifact); err != nil {
return nil, err return nil, err
@ -126,15 +130,16 @@ func ListUploadedArtifactsByRunID(ctx context.Context, runID int64) ([]*ActionAr
type ActionArtifactMeta struct { type ActionArtifactMeta struct {
ArtifactName string ArtifactName string
FileSize int64 FileSize int64
Status int64
} }
// ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run
func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) { func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) {
arts := make([]*ActionArtifactMeta, 0, 10) arts := make([]*ActionArtifactMeta, 0, 10)
return arts, db.GetEngine(ctx).Table("action_artifact"). return arts, db.GetEngine(ctx).Table("action_artifact").
Where("run_id=? AND status=?", runID, ArtifactStatusUploadConfirmed). Where("run_id=? AND (status=? OR status=?)", runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired).
GroupBy("artifact_name"). GroupBy("artifact_name").
Select("artifact_name, sum(file_size) as file_size"). Select("artifact_name, sum(file_size) as file_size, max(status) as status").
Find(&arts) Find(&arts)
} }
@ -149,3 +154,16 @@ func ListArtifactsByRunIDAndName(ctx context.Context, runID int64, name string)
arts := make([]*ActionArtifact, 0, 10) arts := make([]*ActionArtifact, 0, 10)
return arts, db.GetEngine(ctx).Where("run_id=? AND artifact_name=?", runID, name).Find(&arts) return arts, db.GetEngine(ctx).Where("run_id=? AND artifact_name=?", runID, name).Find(&arts)
} }
// ListNeedExpiredArtifacts returns all need expired artifacts but not deleted
func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) {
arts := make([]*ActionArtifact, 0, 10)
return arts, db.GetEngine(ctx).
Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts)
}
// SetArtifactExpired sets an artifact to expired
func SetArtifactExpired(ctx context.Context, artifactID int64) error {
_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)})
return err
}

View File

@ -6,6 +6,7 @@ package actions
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings" "strings"
"time" "time"
@ -34,7 +35,8 @@ type ActionRun struct {
Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository
TriggerUserID int64 `xorm:"index"` TriggerUserID int64 `xorm:"index"`
TriggerUser *user_model.User `xorm:"-"` TriggerUser *user_model.User `xorm:"-"`
Ref string `xorm:"index"` // the commit/tag/… that caused the run ScheduleID int64
Ref string `xorm:"index"` // the commit/tag/… that caused the run
CommitSHA string CommitSHA string
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
NeedApproval bool // may need approval if it's a fork pull request NeedApproval bool // may need approval if it's a fork pull request
@ -350,7 +352,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
// It's impossible that the run is not found, since Gitea never deletes runs. // It's impossible that the run is not found, since Gitea never deletes runs.
} }
if run.Status != 0 || util.SliceContains(cols, "status") { if run.Status != 0 || slices.Contains(cols, "status") {
if run.RepoID == 0 { if run.RepoID == 0 {
run, err = GetRunByID(ctx, run.ID) run, err = GetRunByID(ctx, run.ID)
if err != nil { if err != nil {

View File

@ -6,6 +6,7 @@ package actions
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -107,11 +108,11 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
return 0, err return 0, err
} }
if affected == 0 || (!util.SliceContains(cols, "status") && job.Status == 0) { if affected == 0 || (!slices.Contains(cols, "status") && job.Status == 0) {
return affected, nil return affected, nil
} }
if affected != 0 && util.SliceContains(cols, "status") && job.Status.IsWaiting() { if affected != 0 && slices.Contains(cols, "status") && job.Status.IsWaiting() {
// if the status of job changes to waiting again, increase tasks version. // if the status of job changes to waiting again, increase tasks version.
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil { if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
return 0, err return 0, err

View File

@ -126,6 +126,15 @@ func (at ActionType) String() string {
} }
} }
func (at ActionType) InActions(actions ...string) bool {
for _, action := range actions {
if action == at.String() {
return true
}
}
return false
}
// Action represents user operation type and other information to // Action represents user operation type and other information to
// repository. It implemented interface base.Actioner so that can be // repository. It implemented interface base.Actioner so that can be
// used in template render. // used in template render.

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities" _ "code.gitea.io/gitea/models/activities"
_ "code.gitea.io/gitea/models/auth" _ "code.gitea.io/gitea/models/auth"
_ "code.gitea.io/gitea/models/perm/access" _ "code.gitea.io/gitea/models/perm/access"

View File

@ -153,7 +153,12 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final
return DefaultAvatarLink() return DefaultAvatarLink()
} }
enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar) disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar,
setting.GetDefaultDisableGravatar(),
)
enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar,
setting.GetDefaultEnableFederatedAvatar(disableGravatar))
var err error var err error
if enableFederatedAvatar && system_model.LibravatarService != nil { if enableFederatedAvatar && system_model.LibravatarService != nil {
@ -174,7 +179,6 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final
return urlStr return urlStr
} }
disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar)
if !disableGravatar { if !disableGravatar {
// copy GravatarSourceURL, because we will modify its Path. // copy GravatarSourceURL, because we will modify its Path.
avatarURLCopy := *system_model.GravatarSourceURL avatarURLCopy := *system_model.GravatarSourceURL

View File

@ -1,6 +1,6 @@
- -
id: 1 id: 1
setting_key: 'disable_gravatar' setting_key: 'picture.disable_gravatar'
setting_value: 'false' setting_value: 'false'
version: 1 version: 1
created: 1653533198 created: 1653533198
@ -8,7 +8,7 @@
- -
id: 2 id: 2
setting_key: 'enable_federated_avatar' setting_key: 'picture.enable_federated_avatar'
setting_value: 'false' setting_value: 'false'
version: 1 version: 1
created: 1653533198 created: 1653533198

View File

@ -10,6 +10,8 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@ -7,6 +7,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"slices"
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -435,7 +436,7 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre
whitelist = make([]int64, 0, len(teams)) whitelist = make([]int64, 0, len(teams))
for i := range teams { for i := range teams {
if util.SliceContains(newWhitelist, teams[i].ID) { if slices.Contains(newWhitelist, teams[i].ID) {
whitelist = append(whitelist, teams[i].ID) whitelist = append(whitelist, teams[i].ID)
} }
} }

View File

@ -17,6 +17,7 @@ import (
project_model "code.gitea.io/gitea/models/project" project_model "code.gitea.io/gitea/models/project"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -1247,3 +1248,44 @@ func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) {
func (c *Comment) HasOriginalAuthor() bool { func (c *Comment) HasOriginalAuthor() bool {
return c.OriginalAuthor != "" && c.OriginalAuthorID != 0 return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
} }
// InsertIssueComments inserts many comments of issues.
func InsertIssueComments(comments []*Comment) error {
if len(comments) == 0 {
return nil
}
issueIDs := make(container.Set[int64])
for _, comment := range comments {
issueIDs.Add(comment.IssueID)
}
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
for _, comment := range comments {
if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil {
return err
}
for _, reaction := range comment.Reactions {
reaction.IssueID = comment.IssueID
reaction.CommentID = comment.ID
}
if len(comment.Reactions) > 0 {
if err := db.Insert(ctx, comment.Reactions); err != nil {
return err
}
}
}
for issueID := range issueIDs {
if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?",
issueID, CommentTypeComment, issueID); err != nil {
return err
}
}
return committer.Commit()
}

View File

@ -70,3 +70,30 @@ func TestAsCommentType(t *testing.T) {
assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment")) assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment"))
assert.Equal(t, issues_model.CommentTypePRUnScheduledToAutoMerge, issues_model.AsCommentType("pull_cancel_scheduled_merge")) assert.Equal(t, issues_model.CommentTypePRUnScheduledToAutoMerge, issues_model.AsCommentType("pull_cancel_scheduled_merge"))
} }
func TestMigrate_InsertIssueComments(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
_ = issue.LoadRepo(db.DefaultContext)
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
reaction := &issues_model.Reaction{
Type: "heart",
UserID: owner.ID,
}
comment := &issues_model.Comment{
PosterID: owner.ID,
Poster: owner,
IssueID: issue.ID,
Issue: issue,
Reactions: []*issues_model.Reaction{reaction},
}
err := issues_model.InsertIssueComments([]*issues_model.Comment{comment})
assert.NoError(t, err)
issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments)
unittest.CheckConsistencyFor(t, &issues_model.Issue{})
}

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"fmt" "fmt"
"regexp" "regexp"
"slices"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
project_model "code.gitea.io/gitea/models/project" project_model "code.gitea.io/gitea/models/project"
@ -605,7 +606,7 @@ func IsUserParticipantsOfIssue(user *user_model.User, issue *Issue) bool {
log.Error(err.Error()) log.Error(err.Error())
return false return false
} }
return util.SliceContains(userIDs, user.ID) return slices.Contains(userIDs, user.ID)
} }
// DependencyInfo represents high level information about an issue which is a dependency of another issue. // DependencyInfo represents high level information about an issue which is a dependency of another issue.
@ -630,7 +631,7 @@ func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, erro
Find(&userIDs); err != nil { Find(&userIDs); err != nil {
return nil, fmt.Errorf("get poster IDs: %w", err) return nil, fmt.Errorf("get poster IDs: %w", err)
} }
if !util.SliceContains(userIDs, issue.PosterID) { if !slices.Contains(userIDs, issue.PosterID) {
return append(userIDs, issue.PosterID), nil return append(userIDs, issue.PosterID), nil
} }
return userIDs, nil return userIDs, nil
@ -891,3 +892,50 @@ func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, erro
func IsErrIssueMaxPinReached(err error) bool { func IsErrIssueMaxPinReached(err error) bool {
return err == ErrIssueMaxPinReached return err == ErrIssueMaxPinReached
} }
// InsertIssues insert issues to database
func InsertIssues(issues ...*Issue) error {
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
for _, issue := range issues {
if err := insertIssue(ctx, issue); err != nil {
return err
}
}
return committer.Commit()
}
func insertIssue(ctx context.Context, issue *Issue) error {
sess := db.GetEngine(ctx)
if _, err := sess.NoAutoTime().Insert(issue); err != nil {
return err
}
issueLabels := make([]IssueLabel, 0, len(issue.Labels))
for _, label := range issue.Labels {
issueLabels = append(issueLabels, IssueLabel{
IssueID: issue.ID,
LabelID: label.ID,
})
}
if len(issueLabels) > 0 {
if _, err := sess.Insert(issueLabels); err != nil {
return err
}
}
for _, reaction := range issue.Reactions {
reaction.IssueID = issue.ID
}
if len(issue.Reactions) > 0 {
if _, err := sess.Insert(issue.Reactions); err != nil {
return err
}
}
return nil
}

View File

@ -573,3 +573,45 @@ func TestIssueLoadAttributes(t *testing.T) {
} }
} }
} }
func assertCreateIssues(t *testing.T, isPull bool) {
assert.NoError(t, unittest.PrepareTestDatabase())
reponame := "repo1"
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1})
assert.EqualValues(t, milestone.ID, 1)
reaction := &issues_model.Reaction{
Type: "heart",
UserID: owner.ID,
}
title := "issuetitle1"
is := &issues_model.Issue{
RepoID: repo.ID,
MilestoneID: milestone.ID,
Repo: repo,
Title: title,
Content: "issuecontent1",
IsPull: isPull,
PosterID: owner.ID,
Poster: owner,
IsClosed: true,
Labels: []*issues_model.Label{label},
Reactions: []*issues_model.Reaction{reaction},
}
err := issues_model.InsertIssues(is)
assert.NoError(t, err)
i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title})
unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID})
}
func TestMigrate_CreateIssuesIsPullFalse(t *testing.T) {
assertCreateIssues(t, false)
}
func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) {
assertCreateIssues(t, true)
}

View File

@ -11,6 +11,8 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
_ "code.gitea.io/gitea/models/repo" _ "code.gitea.io/gitea/models/repo"
_ "code.gitea.io/gitea/models/user" _ "code.gitea.io/gitea/models/user"

View File

@ -10,7 +10,6 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -323,261 +322,6 @@ func DeleteMilestoneByRepoID(repoID, id int64) error {
return committer.Commit() return committer.Commit()
} }
// MilestoneList is a list of milestones offering additional functionality
type MilestoneList []*Milestone
func (milestones MilestoneList) getMilestoneIDs() []int64 {
ids := make([]int64, 0, len(milestones))
for _, ms := range milestones {
ids = append(ids, ms.ID)
}
return ids
}
// GetMilestonesOption contain options to get milestones
type GetMilestonesOption struct {
db.ListOptions
RepoID int64
State api.StateType
Name string
SortType string
}
func (opts GetMilestonesOption) toCond() builder.Cond {
cond := builder.NewCond()
if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
switch opts.State {
case api.StateClosed:
cond = cond.And(builder.Eq{"is_closed": true})
case api.StateAll:
break
// api.StateOpen:
default:
cond = cond.And(builder.Eq{"is_closed": false})
}
if len(opts.Name) != 0 {
cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
}
return cond
}
// GetMilestones returns milestones filtered by GetMilestonesOption's
func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) {
sess := db.GetEngine(db.DefaultContext).Where(opts.toCond())
if opts.Page != 0 {
sess = db.SetSessionPagination(sess, &opts)
}
switch opts.SortType {
case "furthestduedate":
sess.Desc("deadline_unix")
case "leastcomplete":
sess.Asc("completeness")
case "mostcomplete":
sess.Desc("completeness")
case "leastissues":
sess.Asc("num_issues")
case "mostissues":
sess.Desc("num_issues")
case "id":
sess.Asc("id")
default:
sess.Asc("deadline_unix").Asc("id")
}
miles := make([]*Milestone, 0, opts.PageSize)
total, err := sess.FindAndCount(&miles)
return miles, total, err
}
// GetMilestoneIDsByNames returns a list of milestone ids by given names.
// It doesn't filter them by repo, so it could return milestones belonging to different repos.
// It's used for filtering issues via indexer, otherwise it would be useless.
// Since it could return milestones with the same name, so the length of returned ids could be more than the length of names.
func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) {
var ids []int64
return ids, db.GetEngine(ctx).Table("milestone").
Where(db.BuildCaseInsensitiveIn("name", names)).
Cols("id").
Find(&ids)
}
// SearchMilestones search milestones
func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) {
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
}
if page > 0 {
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
}
switch sortType {
case "furthestduedate":
sess.Desc("deadline_unix")
case "leastcomplete":
sess.Asc("completeness")
case "mostcomplete":
sess.Desc("completeness")
case "leastissues":
sess.Asc("num_issues")
case "mostissues":
sess.Desc("num_issues")
default:
sess.Asc("deadline_unix")
}
return miles, sess.Find(&miles)
}
// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
return SearchMilestones(
builder.In("repo_id", repoIDs),
page,
isClosed,
sortType,
"",
)
}
// MilestonesStats represents milestone statistic information.
type MilestonesStats struct {
OpenCount, ClosedCount int64
}
// Total returns the total counts of milestones
func (m MilestonesStats) Total() int64 {
return m.OpenCount + m.ClosedCount
}
// GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions.
func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) {
var err error
stats := &MilestonesStats{}
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false)
if repoCond.IsValid() {
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
}
stats.OpenCount, err = sess.Count(new(Milestone))
if err != nil {
return nil, err
}
sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true)
if repoCond.IsValid() {
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
}
stats.ClosedCount, err = sess.Count(new(Milestone))
if err != nil {
return nil, err
}
return stats, nil
}
// GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword.
func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*MilestonesStats, error) {
var err error
stats := &MilestonesStats{}
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false)
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
}
stats.OpenCount, err = sess.Count(new(Milestone))
if err != nil {
return nil, err
}
sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true)
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
}
stats.ClosedCount, err = sess.Count(new(Milestone))
if err != nil {
return nil, err
}
return stats, nil
}
// CountMilestones returns number of milestones in given repository with other options
func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) {
return db.GetEngine(ctx).
Where(opts.toCond()).
Count(new(Milestone))
}
// CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
if repoCond.IsValid() {
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
}
countsSlice := make([]*struct {
RepoID int64
Count int64
}, 0, 10)
if err := sess.GroupBy("repo_id").
Select("repo_id AS repo_id, COUNT(*) AS count").
Table("milestone").
Find(&countsSlice); err != nil {
return nil, err
}
countMap := make(map[int64]int64, len(countsSlice))
for _, c := range countsSlice {
countMap[c.RepoID] = c.Count
}
return countMap, nil
}
// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
func CountMilestonesByRepoCondAndKw(repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) {
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
}
countsSlice := make([]*struct {
RepoID int64
Count int64
}, 0, 10)
if err := sess.GroupBy("repo_id").
Select("repo_id AS repo_id, COUNT(*) AS count").
Table("milestone").
Find(&countsSlice); err != nil {
return nil, err
}
countMap := make(map[int64]int64, len(countsSlice))
for _, c := range countsSlice {
countMap[c.RepoID] = c.Count
}
return countMap, nil
}
func updateRepoMilestoneNum(ctx context.Context, repoID int64) error { func updateRepoMilestoneNum(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?", _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?",
repoID, repoID,
@ -588,53 +332,6 @@ func updateRepoMilestoneNum(ctx context.Context, repoID int64) error {
return err return err
} }
// _____ _ _ _____ _
// |_ _| __ __ _ ___| | _____ __| |_ _(_)_ __ ___ ___ ___
// | || '__/ _` |/ __| |/ / _ \/ _` | | | | | '_ ` _ \ / _ \/ __|
// | || | | (_| | (__| < __/ (_| | | | | | | | | | | __/\__ \
// |_||_| \__,_|\___|_|\_\___|\__,_| |_| |_|_| |_| |_|\___||___/
//
func (milestones MilestoneList) loadTotalTrackedTimes(ctx context.Context) error {
type totalTimesByMilestone struct {
MilestoneID int64
Time int64
}
if len(milestones) == 0 {
return nil
}
trackedTimes := make(map[int64]int64, len(milestones))
// Get total tracked time by milestone_id
rows, err := db.GetEngine(ctx).Table("issue").
Join("INNER", "milestone", "issue.milestone_id = milestone.id").
Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
Where("tracked_time.deleted = ?", false).
Select("milestone_id, sum(time) as time").
In("milestone_id", milestones.getMilestoneIDs()).
GroupBy("milestone_id").
Rows(new(totalTimesByMilestone))
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var totalTime totalTimesByMilestone
err = rows.Scan(&totalTime)
if err != nil {
return err
}
trackedTimes[totalTime.MilestoneID] = totalTime.Time
}
for _, milestone := range milestones {
milestone.TotalTrackedTime = trackedTimes[milestone.ID]
}
return nil
}
func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error { func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error {
type totalTimesByMilestone struct { type totalTimesByMilestone struct {
MilestoneID int64 MilestoneID int64
@ -658,12 +355,33 @@ func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error {
return nil return nil
} }
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
func (milestones MilestoneList) LoadTotalTrackedTimes() error {
return milestones.loadTotalTrackedTimes(db.DefaultContext)
}
// LoadTotalTrackedTime loads the tracked time for the milestone // LoadTotalTrackedTime loads the tracked time for the milestone
func (m *Milestone) LoadTotalTrackedTime() error { func (m *Milestone) LoadTotalTrackedTime() error {
return m.loadTotalTrackedTime(db.DefaultContext) return m.loadTotalTrackedTime(db.DefaultContext)
} }
// InsertMilestones creates milestones of repository.
func InsertMilestones(ms ...*Milestone) (err error) {
if len(ms) == 0 {
return nil
}
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
// to return the id, so we should not use batch insert
for _, m := range ms {
if _, err = sess.NoAutoTime().Insert(m); err != nil {
return err
}
}
if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil {
return err
}
return committer.Commit()
}

View File

@ -0,0 +1,315 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"context"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"xorm.io/builder"
)
// MilestoneList is a list of milestones offering additional functionality
type MilestoneList []*Milestone
func (milestones MilestoneList) getMilestoneIDs() []int64 {
ids := make([]int64, 0, len(milestones))
for _, ms := range milestones {
ids = append(ids, ms.ID)
}
return ids
}
// GetMilestonesOption contain options to get milestones
type GetMilestonesOption struct {
db.ListOptions
RepoID int64
State api.StateType
Name string
SortType string
}
func (opts GetMilestonesOption) toCond() builder.Cond {
cond := builder.NewCond()
if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
switch opts.State {
case api.StateClosed:
cond = cond.And(builder.Eq{"is_closed": true})
case api.StateAll:
break
// api.StateOpen:
default:
cond = cond.And(builder.Eq{"is_closed": false})
}
if len(opts.Name) != 0 {
cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
}
return cond
}
// GetMilestones returns milestones filtered by GetMilestonesOption's
func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) {
sess := db.GetEngine(db.DefaultContext).Where(opts.toCond())
if opts.Page != 0 {
sess = db.SetSessionPagination(sess, &opts)
}
switch opts.SortType {
case "furthestduedate":
sess.Desc("deadline_unix")
case "leastcomplete":
sess.Asc("completeness")
case "mostcomplete":
sess.Desc("completeness")
case "leastissues":
sess.Asc("num_issues")
case "mostissues":
sess.Desc("num_issues")
case "id":
sess.Asc("id")
default:
sess.Asc("deadline_unix").Asc("id")
}
miles := make([]*Milestone, 0, opts.PageSize)
total, err := sess.FindAndCount(&miles)
return miles, total, err
}
// GetMilestoneIDsByNames returns a list of milestone ids by given names.
// It doesn't filter them by repo, so it could return milestones belonging to different repos.
// It's used for filtering issues via indexer, otherwise it would be useless.
// Since it could return milestones with the same name, so the length of returned ids could be more than the length of names.
func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) {
var ids []int64
return ids, db.GetEngine(ctx).Table("milestone").
Where(db.BuildCaseInsensitiveIn("name", names)).
Cols("id").
Find(&ids)
}
// SearchMilestones search milestones
func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) {
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
}
if page > 0 {
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
}
switch sortType {
case "furthestduedate":
sess.Desc("deadline_unix")
case "leastcomplete":
sess.Asc("completeness")
case "mostcomplete":
sess.Desc("completeness")
case "leastissues":
sess.Asc("num_issues")
case "mostissues":
sess.Desc("num_issues")
default:
sess.Asc("deadline_unix")
}
return miles, sess.Find(&miles)
}
// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
return SearchMilestones(
builder.In("repo_id", repoIDs),
page,
isClosed,
sortType,
"",
)
}
func (milestones MilestoneList) loadTotalTrackedTimes(ctx context.Context) error {
type totalTimesByMilestone struct {
MilestoneID int64
Time int64
}
if len(milestones) == 0 {
return nil
}
trackedTimes := make(map[int64]int64, len(milestones))
// Get total tracked time by milestone_id
rows, err := db.GetEngine(ctx).Table("issue").
Join("INNER", "milestone", "issue.milestone_id = milestone.id").
Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
Where("tracked_time.deleted = ?", false).
Select("milestone_id, sum(time) as time").
In("milestone_id", milestones.getMilestoneIDs()).
GroupBy("milestone_id").
Rows(new(totalTimesByMilestone))
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var totalTime totalTimesByMilestone
err = rows.Scan(&totalTime)
if err != nil {
return err
}
trackedTimes[totalTime.MilestoneID] = totalTime.Time
}
for _, milestone := range milestones {
milestone.TotalTrackedTime = trackedTimes[milestone.ID]
}
return nil
}
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
func (milestones MilestoneList) LoadTotalTrackedTimes() error {
return milestones.loadTotalTrackedTimes(db.DefaultContext)
}
// CountMilestones returns number of milestones in given repository with other options
func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) {
return db.GetEngine(ctx).
Where(opts.toCond()).
Count(new(Milestone))
}
// CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
if repoCond.IsValid() {
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
}
countsSlice := make([]*struct {
RepoID int64
Count int64
}, 0, 10)
if err := sess.GroupBy("repo_id").
Select("repo_id AS repo_id, COUNT(*) AS count").
Table("milestone").
Find(&countsSlice); err != nil {
return nil, err
}
countMap := make(map[int64]int64, len(countsSlice))
for _, c := range countsSlice {
countMap[c.RepoID] = c.Count
}
return countMap, nil
}
// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
func CountMilestonesByRepoCondAndKw(repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) {
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
}
countsSlice := make([]*struct {
RepoID int64
Count int64
}, 0, 10)
if err := sess.GroupBy("repo_id").
Select("repo_id AS repo_id, COUNT(*) AS count").
Table("milestone").
Find(&countsSlice); err != nil {
return nil, err
}
countMap := make(map[int64]int64, len(countsSlice))
for _, c := range countsSlice {
countMap[c.RepoID] = c.Count
}
return countMap, nil
}
// MilestonesStats represents milestone statistic information.
type MilestonesStats struct {
OpenCount, ClosedCount int64
}
// Total returns the total counts of milestones
func (m MilestonesStats) Total() int64 {
return m.OpenCount + m.ClosedCount
}
// GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions.
func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) {
var err error
stats := &MilestonesStats{}
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false)
if repoCond.IsValid() {
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
}
stats.OpenCount, err = sess.Count(new(Milestone))
if err != nil {
return nil, err
}
sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true)
if repoCond.IsValid() {
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
}
stats.ClosedCount, err = sess.Count(new(Milestone))
if err != nil {
return nil, err
}
return stats, nil
}
// GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword.
func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*MilestonesStats, error) {
var err error
stats := &MilestonesStats{}
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false)
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
}
stats.OpenCount, err = sess.Count(new(Milestone))
if err != nil {
return nil, err
}
sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true)
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
}
stats.ClosedCount, err = sess.Count(new(Milestone))
if err != nil {
return nil, err
}
return stats, nil
}

View File

@ -351,3 +351,21 @@ func TestUpdateMilestoneCounters(t *testing.T) {
assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID))
unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
} }
func TestMigrate_InsertMilestones(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
reponame := "repo1"
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
name := "milestonetest1"
ms := &issues_model.Milestone{
RepoID: repo.ID,
Name: name,
}
err := issues_model.InsertMilestones(ms)
assert.NoError(t, err)
unittest.AssertExistsAndLoadBean(t, ms)
repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones)
unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
}

View File

@ -1105,3 +1105,23 @@ func TokenizeCodeOwnersLine(line string) []string {
return tokens return tokens
} }
// InsertPullRequests inserted pull requests
func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
for _, pr := range prs {
if err := insertIssue(ctx, pr.Issue); err != nil {
return err
}
pr.IssueID = pr.Issue.ID
if _, err := sess.NoAutoTime().Insert(pr); err != nil {
return err
}
}
return committer.Commit()
}

View File

@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -337,3 +338,31 @@ func TestGetApprovers(t *testing.T) {
expected := "Reviewed-by: User Five <user5@example.com>\nReviewed-by: User Six <user6@example.com>\n" expected := "Reviewed-by: User Five <user5@example.com>\nReviewed-by: User Six <user6@example.com>\n"
assert.EqualValues(t, expected, approvers) assert.EqualValues(t, expected, approvers)
} }
func TestMigrate_InsertPullRequests(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
reponame := "repo1"
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
i := &issues_model.Issue{
RepoID: repo.ID,
Repo: repo,
Title: "title1",
Content: "issuecontent1",
IsPull: true,
PosterID: owner.ID,
Poster: owner,
}
p := &issues_model.PullRequest{
Issue: i,
}
err := issues_model.InsertPullRequests(db.DefaultContext, p)
assert.NoError(t, err)
_ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID})
unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{})
}

View File

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/system" _ "code.gitea.io/gitea/models/system"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"

View File

@ -1,196 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package models
import (
"context"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/structs"
)
// InsertMilestones creates milestones of repository.
func InsertMilestones(ms ...*issues_model.Milestone) (err error) {
if len(ms) == 0 {
return nil
}
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
// to return the id, so we should not use batch insert
for _, m := range ms {
if _, err = sess.NoAutoTime().Insert(m); err != nil {
return err
}
}
if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil {
return err
}
return committer.Commit()
}
// InsertIssues insert issues to database
func InsertIssues(issues ...*issues_model.Issue) error {
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
for _, issue := range issues {
if err := insertIssue(ctx, issue); err != nil {
return err
}
}
return committer.Commit()
}
func insertIssue(ctx context.Context, issue *issues_model.Issue) error {
sess := db.GetEngine(ctx)
if _, err := sess.NoAutoTime().Insert(issue); err != nil {
return err
}
issueLabels := make([]issues_model.IssueLabel, 0, len(issue.Labels))
for _, label := range issue.Labels {
issueLabels = append(issueLabels, issues_model.IssueLabel{
IssueID: issue.ID,
LabelID: label.ID,
})
}
if len(issueLabels) > 0 {
if _, err := sess.Insert(issueLabels); err != nil {
return err
}
}
for _, reaction := range issue.Reactions {
reaction.IssueID = issue.ID
}
if len(issue.Reactions) > 0 {
if _, err := sess.Insert(issue.Reactions); err != nil {
return err
}
}
return nil
}
// InsertIssueComments inserts many comments of issues.
func InsertIssueComments(comments []*issues_model.Comment) error {
if len(comments) == 0 {
return nil
}
issueIDs := make(container.Set[int64])
for _, comment := range comments {
issueIDs.Add(comment.IssueID)
}
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
for _, comment := range comments {
if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil {
return err
}
for _, reaction := range comment.Reactions {
reaction.IssueID = comment.IssueID
reaction.CommentID = comment.ID
}
if len(comment.Reactions) > 0 {
if err := db.Insert(ctx, comment.Reactions); err != nil {
return err
}
}
}
for issueID := range issueIDs {
if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?",
issueID, issues_model.CommentTypeComment, issueID); err != nil {
return err
}
}
return committer.Commit()
}
// InsertPullRequests inserted pull requests
func InsertPullRequests(ctx context.Context, prs ...*issues_model.PullRequest) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
for _, pr := range prs {
if err := insertIssue(ctx, pr.Issue); err != nil {
return err
}
pr.IssueID = pr.Issue.ID
if _, err := sess.NoAutoTime().Insert(pr); err != nil {
return err
}
}
return committer.Commit()
}
// InsertReleases migrates release
func InsertReleases(rels ...*repo_model.Release) error {
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
for _, rel := range rels {
if _, err := sess.NoAutoTime().Insert(rel); err != nil {
return err
}
if len(rel.Attachments) > 0 {
for i := range rel.Attachments {
rel.Attachments[i].ReleaseID = rel.ID
}
if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil {
return err
}
}
}
return committer.Commit()
}
// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID
func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error {
if err := issues_model.UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil {
return err
}
if err := issues_model.UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil {
return err
}
if err := repo_model.UpdateReleasesMigrationsByType(tp, externalUserID, userID); err != nil {
return err
}
if err := issues_model.UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil {
return err
}
return issues_model.UpdateReviewsMigrationsByType(tp, externalUserID, userID)
}

View File

@ -1,145 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package models
import (
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestMigrate_InsertMilestones(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
reponame := "repo1"
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
name := "milestonetest1"
ms := &issues_model.Milestone{
RepoID: repo.ID,
Name: name,
}
err := InsertMilestones(ms)
assert.NoError(t, err)
unittest.AssertExistsAndLoadBean(t, ms)
repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones)
unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
}
func assertCreateIssues(t *testing.T, isPull bool) {
assert.NoError(t, unittest.PrepareTestDatabase())
reponame := "repo1"
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1})
assert.EqualValues(t, milestone.ID, 1)
reaction := &issues_model.Reaction{
Type: "heart",
UserID: owner.ID,
}
title := "issuetitle1"
is := &issues_model.Issue{
RepoID: repo.ID,
MilestoneID: milestone.ID,
Repo: repo,
Title: title,
Content: "issuecontent1",
IsPull: isPull,
PosterID: owner.ID,
Poster: owner,
IsClosed: true,
Labels: []*issues_model.Label{label},
Reactions: []*issues_model.Reaction{reaction},
}
err := InsertIssues(is)
assert.NoError(t, err)
i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title})
unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID})
}
func TestMigrate_CreateIssuesIsPullFalse(t *testing.T) {
assertCreateIssues(t, false)
}
func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) {
assertCreateIssues(t, true)
}
func TestMigrate_InsertIssueComments(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
_ = issue.LoadRepo(db.DefaultContext)
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
reaction := &issues_model.Reaction{
Type: "heart",
UserID: owner.ID,
}
comment := &issues_model.Comment{
PosterID: owner.ID,
Poster: owner,
IssueID: issue.ID,
Issue: issue,
Reactions: []*issues_model.Reaction{reaction},
}
err := InsertIssueComments([]*issues_model.Comment{comment})
assert.NoError(t, err)
issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments)
unittest.CheckConsistencyFor(t, &issues_model.Issue{})
}
func TestMigrate_InsertPullRequests(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
reponame := "repo1"
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
i := &issues_model.Issue{
RepoID: repo.ID,
Repo: repo,
Title: "title1",
Content: "issuecontent1",
IsPull: true,
PosterID: owner.ID,
Poster: owner,
}
p := &issues_model.PullRequest{
Issue: i,
}
err := InsertPullRequests(db.DefaultContext, p)
assert.NoError(t, err)
_ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID})
unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{})
}
func TestMigrate_InsertReleases(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
a := &repo_model.Attachment{
UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12",
}
r := &repo_model.Release{
Attachments: []*repo_model.Attachment{a},
}
err := InsertReleases(r)
assert.NoError(t, err)
}

View File

@ -528,6 +528,10 @@ var migrations = []Migration{
NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable), NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable),
// v273 -> v274 // v273 -> v274
NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable), NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable),
// v274 -> v275
NewMigration("Add Actions artifacts expiration date", v1_21.AddExpiredUnixColumnInActionArtifactTable),
// v275 -> v276
NewMigration("Add ScheduleID for ActionRun", v1_21.AddScheduleIDForActionRun),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

View File

@ -0,0 +1,36 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_21 //nolint
import (
"time"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
func AddExpiredUnixColumnInActionArtifactTable(x *xorm.Engine) error {
type ActionArtifact struct {
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // time when the artifact will be expired
}
if err := x.Sync(new(ActionArtifact)); err != nil {
return err
}
return updateArtifactsExpiredUnixTo90Days(x)
}
func updateArtifactsExpiredUnixTo90Days(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
expiredTime := time.Now().AddDate(0, 0, 90).Unix()
if _, err := sess.Exec(`UPDATE action_artifact SET expired_unix=? WHERE status='2' AND expired_unix is NULL`, expiredTime); err != nil {
return err
}
return sess.Commit()
}

View File

@ -0,0 +1,15 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_21 //nolint
import (
"xorm.io/xorm"
)
func AddScheduleIDForActionRun(x *xorm.Engine) error {
type ActionRun struct {
ScheduleID int64
}
return x.Sync(new(ActionRun))
}

View File

@ -151,85 +151,6 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error
return nil return nil
} }
// HasRepository returns true if given repository belong to team.
func HasRepository(t *organization.Team, repoID int64) bool {
return organization.HasTeamRepo(db.DefaultContext, t.OrgID, t.ID, repoID)
}
// removeRepository removes a repository from a team and recalculates access
// Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted)
func removeRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository, recalculate bool) (err error) {
e := db.GetEngine(ctx)
if err = organization.RemoveTeamRepo(ctx, t.ID, repo.ID); err != nil {
return err
}
t.NumRepos--
if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil {
return err
}
// Don't need to recalculate when delete a repository from organization.
if recalculate {
if err = access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil {
return err
}
}
teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID)
if err != nil {
return fmt.Errorf("getTeamUsersByTeamID: %w", err)
}
for _, teamUser := range teamUsers {
has, err := access_model.HasAccess(ctx, teamUser.UID, repo)
if err != nil {
return err
} else if has {
continue
}
if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repositories
if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
return err
}
}
return nil
}
// RemoveRepository removes repository from team of organization.
// If the team shall include all repositories the request is ignored.
func RemoveRepository(t *organization.Team, repoID int64) error {
if !HasRepository(t, repoID) {
return nil
}
if t.IncludesAllRepositories {
return nil
}
repo, err := repo_model.GetRepositoryByID(db.DefaultContext, repoID)
if err != nil {
return err
}
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
if err = removeRepository(ctx, t, repo, true); err != nil {
return err
}
return committer.Commit()
}
// NewTeam creates a record of new team. // NewTeam creates a record of new team.
// It's caller's responsibility to assign organization ID. // It's caller's responsibility to assign organization ID.
func NewTeam(t *organization.Team) (err error) { func NewTeam(t *organization.Team) (err error) {
@ -564,12 +485,12 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
} }
// Remove watches from now unaccessible // Remove watches from now unaccessible
if err := reconsiderWatches(ctx, repo, userID); err != nil { if err := ReconsiderWatches(ctx, repo, userID); err != nil {
return err return err
} }
// Remove issue assignments from now unaccessible // Remove issue assignments from now unaccessible
if err := reconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
return err return err
} }
} }
@ -602,3 +523,33 @@ func RemoveTeamMember(team *organization.Team, userID int64) error {
} }
return committer.Commit() return committer.Commit()
} }
func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error {
user, err := user_model.GetUserByID(ctx, uid)
if err != nil {
return err
}
if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
return err
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}).
In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
Delete(&issues_model.IssueAssignees{}); err != nil {
return fmt.Errorf("Could not delete assignee[%d] %w", uid, err)
}
return nil
}
func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error {
if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has {
return err
}
if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repository
return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID)
}

View File

@ -51,36 +51,6 @@ func TestTeam_RemoveMember(t *testing.T) {
assert.True(t, organization.IsErrLastOrgOwner(err)) assert.True(t, organization.IsErrLastOrgOwner(err))
} }
func TestTeam_HasRepository(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
test := func(teamID, repoID int64, expected bool) {
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
assert.Equal(t, expected, HasRepository(team, repoID))
}
test(1, 1, false)
test(1, 3, true)
test(1, 5, true)
test(1, unittest.NonexistentID, false)
test(2, 3, true)
test(2, 5, false)
}
func TestTeam_RemoveRepository(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
testSuccess := func(teamID, repoID int64) {
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
assert.NoError(t, RemoveRepository(team, repoID))
unittest.AssertNotExistsBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repoID})
unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &repo_model.Repository{ID: repoID})
}
testSuccess(2, 3)
testSuccess(2, 5)
testSuccess(1, unittest.NonexistentID)
}
func TestIsUsableTeamName(t *testing.T) { func TestIsUsableTeamName(t *testing.T) {
assert.NoError(t, organization.IsUsableTeamName("usable")) assert.NoError(t, organization.IsUsableTeamName("usable"))
assert.True(t, db.IsErrNameReserved(organization.IsUsableTeamName("new"))) assert.True(t, db.IsErrNameReserved(organization.IsUsableTeamName("new")))

View File

@ -10,6 +10,8 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
_ "code.gitea.io/gitea/models/organization" _ "code.gitea.io/gitea/models/organization"
_ "code.gitea.io/gitea/models/repo" _ "code.gitea.io/gitea/models/repo"
_ "code.gitea.io/gitea/models/user" _ "code.gitea.io/gitea/models/user"

View File

@ -13,6 +13,8 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View File

@ -10,6 +10,8 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
_ "code.gitea.io/gitea/models/repo" _ "code.gitea.io/gitea/models/repo"
_ "code.gitea.io/gitea/models/user" _ "code.gitea.io/gitea/models/user"
) )

View File

@ -11,28 +11,15 @@ import (
_ "image/jpeg" // Needed for jpeg support _ "image/jpeg" // Needed for jpeg support
actions_model "code.gitea.io/gitea/models/actions"
activities_model "code.gitea.io/gitea/models/activities"
admin_model "code.gitea.io/gitea/models/admin"
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
system_model "code.gitea.io/gitea/models/system" system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
actions_module "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage"
"xorm.io/builder"
) )
// Init initialize model // Init initialize model
@ -43,319 +30,6 @@ func Init(ctx context.Context) error {
return system_model.Init(ctx) return system_model.Init(ctx)
} }
// DeleteRepository deletes a repository for a user or organization.
// make sure if you call this func to close open sessions (sqlite will otherwise get a deadlock)
func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
// Query the action tasks of this repo, they will be needed after they have been deleted to remove the logs
tasks, err := actions_model.FindTasks(ctx, actions_model.FindTaskOptions{RepoID: repoID})
if err != nil {
return fmt.Errorf("find actions tasks of repo %v: %w", repoID, err)
}
// Query the artifacts of this repo, they will be needed after they have been deleted to remove artifacts files in ObjectStorage
artifacts, err := actions_model.ListArtifactsByRepoID(ctx, repoID)
if err != nil {
return fmt.Errorf("list actions artifacts of repo %v: %w", repoID, err)
}
// In case is a organization.
org, err := user_model.GetUserByID(ctx, uid)
if err != nil {
return err
}
repo := &repo_model.Repository{OwnerID: uid}
has, err := sess.ID(repoID).Get(repo)
if err != nil {
return err
} else if !has {
return repo_model.ErrRepoNotExist{
ID: repoID,
UID: uid,
OwnerName: "",
Name: "",
}
}
// Delete Deploy Keys
deployKeys, err := asymkey_model.ListDeployKeys(ctx, &asymkey_model.ListDeployKeysOptions{RepoID: repoID})
if err != nil {
return fmt.Errorf("listDeployKeys: %w", err)
}
needRewriteKeysFile := len(deployKeys) > 0
for _, dKey := range deployKeys {
if err := DeleteDeployKey(ctx, doer, dKey.ID); err != nil {
return fmt.Errorf("deleteDeployKeys: %w", err)
}
}
if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil {
return err
} else if cnt != 1 {
return repo_model.ErrRepoNotExist{
ID: repoID,
UID: uid,
OwnerName: "",
Name: "",
}
}
if org.IsOrganization() {
teams, err := organization.FindOrgTeams(ctx, org.ID)
if err != nil {
return err
}
for _, t := range teams {
if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID) {
continue
} else if err = removeRepository(ctx, t, repo, false); err != nil {
return err
}
}
}
attachments := make([]*repo_model.Attachment, 0, 20)
if err = sess.Join("INNER", "`release`", "`release`.id = `attachment`.release_id").
Where("`release`.repo_id = ?", repoID).
Find(&attachments); err != nil {
return err
}
releaseAttachments := make([]string, 0, len(attachments))
for i := 0; i < len(attachments); i++ {
releaseAttachments = append(releaseAttachments, attachments[i].RelativePath())
}
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars=num_stars-1 WHERE id IN (SELECT `uid` FROM `star` WHERE repo_id = ?)", repo.ID); err != nil {
return err
}
if _, err := db.GetEngine(ctx).In("hook_id", builder.Select("id").From("webhook").Where(builder.Eq{"webhook.repo_id": repo.ID})).
Delete(&webhook.HookTask{}); err != nil {
return err
}
if err := db.DeleteBeans(ctx,
&access_model.Access{RepoID: repo.ID},
&activities_model.Action{RepoID: repo.ID},
&repo_model.Collaboration{RepoID: repoID},
&issues_model.Comment{RefRepoID: repoID},
&git_model.CommitStatus{RepoID: repoID},
&git_model.Branch{RepoID: repoID},
&git_model.LFSLock{RepoID: repoID},
&repo_model.LanguageStat{RepoID: repoID},
&issues_model.Milestone{RepoID: repoID},
&repo_model.Mirror{RepoID: repoID},
&activities_model.Notification{RepoID: repoID},
&git_model.ProtectedBranch{RepoID: repoID},
&git_model.ProtectedTag{RepoID: repoID},
&repo_model.PushMirror{RepoID: repoID},
&repo_model.Release{RepoID: repoID},
&repo_model.RepoIndexerStatus{RepoID: repoID},
&repo_model.Redirect{RedirectRepoID: repoID},
&repo_model.RepoUnit{RepoID: repoID},
&repo_model.Star{RepoID: repoID},
&admin_model.Task{RepoID: repoID},
&repo_model.Watch{RepoID: repoID},
&webhook.Webhook{RepoID: repoID},
&secret_model.Secret{RepoID: repoID},
&actions_model.ActionTaskStep{RepoID: repoID},
&actions_model.ActionTask{RepoID: repoID},
&actions_model.ActionRunJob{RepoID: repoID},
&actions_model.ActionRun{RepoID: repoID},
&actions_model.ActionRunner{RepoID: repoID},
&actions_model.ActionScheduleSpec{RepoID: repoID},
&actions_model.ActionSchedule{RepoID: repoID},
&actions_model.ActionArtifact{RepoID: repoID},
); err != nil {
return fmt.Errorf("deleteBeans: %w", err)
}
// Delete Labels and related objects
if err := issues_model.DeleteLabelsByRepoID(ctx, repoID); err != nil {
return err
}
// Delete Pulls and related objects
if err := issues_model.DeletePullsByBaseRepoID(ctx, repoID); err != nil {
return err
}
// Delete Issues and related objects
var attachmentPaths []string
if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil {
return err
}
// Delete issue index
if err := db.DeleteResourceIndex(ctx, "issue_index", repoID); err != nil {
return err
}
if repo.IsFork {
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil {
return fmt.Errorf("decrease fork count: %w", err)
}
}
if _, err := db.Exec(ctx, "UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", uid); err != nil {
return err
}
if len(repo.Topics) > 0 {
if err := repo_model.RemoveTopicsFromRepo(ctx, repo.ID); err != nil {
return err
}
}
if err := project_model.DeleteProjectByRepoID(ctx, repoID); err != nil {
return fmt.Errorf("unable to delete projects for repo[%d]: %w", repoID, err)
}
// Remove LFS objects
var lfsObjects []*git_model.LFSMetaObject
if err = sess.Where("repository_id=?", repoID).Find(&lfsObjects); err != nil {
return err
}
lfsPaths := make([]string, 0, len(lfsObjects))
for _, v := range lfsObjects {
count, err := db.CountByBean(ctx, &git_model.LFSMetaObject{Pointer: lfs.Pointer{Oid: v.Oid}})
if err != nil {
return err
}
if count > 1 {
continue
}
lfsPaths = append(lfsPaths, v.RelativePath())
}
if _, err := db.DeleteByBean(ctx, &git_model.LFSMetaObject{RepositoryID: repoID}); err != nil {
return err
}
// Remove archives
var archives []*repo_model.RepoArchiver
if err = sess.Where("repo_id=?", repoID).Find(&archives); err != nil {
return err
}
archivePaths := make([]string, 0, len(archives))
for _, v := range archives {
archivePaths = append(archivePaths, v.RelativePath())
}
if _, err := db.DeleteByBean(ctx, &repo_model.RepoArchiver{RepoID: repoID}); err != nil {
return err
}
if repo.NumForks > 0 {
if _, err = sess.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil {
log.Error("reset 'fork_id' and 'is_fork': %v", err)
}
}
// Get all attachments with both issue_id and release_id are zero
var newAttachments []*repo_model.Attachment
if err := sess.Where(builder.Eq{
"repo_id": repo.ID,
"issue_id": 0,
"release_id": 0,
}).Find(&newAttachments); err != nil {
return err
}
newAttachmentPaths := make([]string, 0, len(newAttachments))
for _, attach := range newAttachments {
newAttachmentPaths = append(newAttachmentPaths, attach.RelativePath())
}
if _, err := sess.Where("repo_id=?", repo.ID).Delete(new(repo_model.Attachment)); err != nil {
return err
}
if err = committer.Commit(); err != nil {
return err
}
committer.Close()
if needRewriteKeysFile {
if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
log.Error("RewriteAllPublicKeys failed: %v", err)
}
}
// We should always delete the files after the database transaction succeed. If
// we delete the file but the database rollback, the repository will be broken.
// Remove repository files.
repoPath := repo.RepoPath()
system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository files", repoPath)
// Remove wiki files
if repo.HasWiki() {
system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath())
}
// Remove archives
for _, archive := range archivePaths {
system_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archive)
}
// Remove lfs objects
for _, lfsObj := range lfsPaths {
system_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsObj)
}
// Remove issue attachment files.
for _, attachment := range attachmentPaths {
system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachment)
}
// Remove release attachment files.
for _, releaseAttachment := range releaseAttachments {
system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachment)
}
// Remove attachment with no issue_id and release_id.
for _, newAttachment := range newAttachmentPaths {
system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachment)
}
if len(repo.Avatar) > 0 {
if err := storage.RepoAvatars.Delete(repo.CustomAvatarRelativePath()); err != nil {
return fmt.Errorf("Failed to remove %s: %w", repo.Avatar, err)
}
}
// Finally, delete action logs after the actions have already been deleted to avoid new log files
for _, task := range tasks {
err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename)
if err != nil {
log.Error("remove log file %q: %v", task.LogFilename, err)
// go on
}
}
// delete actions artifacts in ObjectStorage after the repo have already been deleted
for _, art := range artifacts {
if err := storage.ActionsArtifacts.Delete(art.StoragePath); err != nil {
log.Error("remove artifact file %q: %v", art.StoragePath, err)
// go on
}
}
return nil
}
type repoChecker struct { type repoChecker struct {
querySQL func(ctx context.Context) ([]map[string][]byte, error) querySQL func(ctx context.Context) ([]map[string][]byte, error)
correctSQL func(ctx context.Context, id int64) error correctSQL func(ctx context.Context, id int64) error

View File

@ -9,7 +9,9 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models" // register table model _ "code.gitea.io/gitea/models" // register table model
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
_ "code.gitea.io/gitea/models/perm/access" // register table model _ "code.gitea.io/gitea/models/perm/access" // register table model
_ "code.gitea.io/gitea/models/repo" // register table model _ "code.gitea.io/gitea/models/repo" // register table model
_ "code.gitea.io/gitea/models/user" // register table model _ "code.gitea.io/gitea/models/user" // register table model

View File

@ -552,3 +552,31 @@ func (r *Release) GetExternalName() string { return r.OriginalAuthor }
// ExternalID ExternalUserRemappable interface // ExternalID ExternalUserRemappable interface
func (r *Release) GetExternalID() int64 { return r.OriginalAuthorID } func (r *Release) GetExternalID() int64 { return r.OriginalAuthorID }
// InsertReleases migrates release
func InsertReleases(rels ...*Release) error {
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
for _, rel := range rels {
if _, err := sess.NoAutoTime().Insert(rel); err != nil {
return err
}
if len(rel.Attachments) > 0 {
for i := range rel.Attachments {
rel.Attachments[i].ReleaseID = rel.ID
}
if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil {
return err
}
}
}
return committer.Commit()
}

View File

@ -0,0 +1,26 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestMigrate_InsertReleases(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
a := &Attachment{
UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12",
}
r := &Release{
Attachments: []*Attachment{a},
}
err := InsertReleases(r)
assert.NoError(t, err)
}

View File

@ -6,6 +6,7 @@ package repo
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -176,7 +177,7 @@ func (cfg *ActionsConfig) ToString() string {
} }
func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool {
return util.SliceContains(cfg.DisabledWorkflows, file) return slices.Contains(cfg.DisabledWorkflows, file)
} }
func (cfg *ActionsConfig) DisableWorkflow(file string) { func (cfg *ActionsConfig) DisableWorkflow(file string) {

View File

@ -1,83 +0,0 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// SPDX-License-Identifier: MIT
package models
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"xorm.io/builder"
)
// DeleteCollaboration removes collaboration relation between the user and repository.
func DeleteCollaboration(repo *repo_model.Repository, uid int64) (err error) {
collaboration := &repo_model.Collaboration{
RepoID: repo.ID,
UserID: uid,
}
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
if has, err := db.GetEngine(ctx).Delete(collaboration); err != nil || has == 0 {
return err
} else if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
return err
}
if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
return err
}
if err = reconsiderWatches(ctx, repo, uid); err != nil {
return err
}
// Unassign a user from any issue (s)he has been assigned to in the repository
if err := reconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil {
return err
}
return committer.Commit()
}
func reconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error {
user, err := user_model.GetUserByID(ctx, uid)
if err != nil {
return err
}
if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
return err
}
if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}).
In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
Delete(&issues_model.IssueAssignees{}); err != nil {
return fmt.Errorf("Could not delete assignee[%d] %w", uid, err)
}
return nil
}
func reconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error {
if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has {
return err
}
if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repository
return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID)
}

View File

@ -9,7 +9,9 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models" // register models _ "code.gitea.io/gitea/models" // register models
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
_ "code.gitea.io/gitea/models/system" // register models of system _ "code.gitea.io/gitea/models/system" // register models of system
) )

View File

@ -94,11 +94,14 @@ func GetSetting(ctx context.Context, key string) (*Setting, error) {
const contextCacheKey = "system_setting" const contextCacheKey = "system_setting"
// GetSettingWithCache returns the setting value via the key // GetSettingWithCache returns the setting value via the key
func GetSettingWithCache(ctx context.Context, key string) (string, error) { func GetSettingWithCache(ctx context.Context, key, defaultVal string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) { return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) { return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSetting(ctx, key) res, err := GetSetting(ctx, key)
if err != nil { if err != nil {
if IsErrSettingIsNotExist(err) {
return defaultVal, nil
}
return "", err return "", err
} }
return res.SettingValue, nil return res.SettingValue, nil
@ -108,17 +111,21 @@ func GetSettingWithCache(ctx context.Context, key string) (string, error) {
// GetSettingBool return bool value of setting, // GetSettingBool return bool value of setting,
// none existing keys and errors are ignored and result in false // none existing keys and errors are ignored and result in false
func GetSettingBool(ctx context.Context, key string) bool { func GetSettingBool(ctx context.Context, key string, defaultVal bool) (bool, error) {
s, _ := GetSetting(ctx, key) s, err := GetSetting(ctx, key)
if s == nil { switch {
return false case err == nil:
v, _ := strconv.ParseBool(s.SettingValue)
return v, nil
case IsErrSettingIsNotExist(err):
return defaultVal, nil
default:
return false, err
} }
v, _ := strconv.ParseBool(s.SettingValue)
return v
} }
func GetSettingWithCacheBool(ctx context.Context, key string) bool { func GetSettingWithCacheBool(ctx context.Context, key string, defaultVal bool) bool {
s, _ := GetSettingWithCache(ctx, key) s, _ := GetSettingWithCache(ctx, key, strconv.FormatBool(defaultVal))
v, _ := strconv.ParseBool(s) v, _ := strconv.ParseBool(s)
return v return v
} }
@ -259,52 +266,41 @@ var (
) )
func Init(ctx context.Context) error { func Init(ctx context.Context) error {
var disableGravatar bool disableGravatar, err := GetSettingBool(ctx, KeyPictureDisableGravatar, setting_module.GetDefaultDisableGravatar())
disableGravatarSetting, err := GetSetting(ctx, KeyPictureDisableGravatar) if err != nil {
if IsErrSettingIsNotExist(err) {
disableGravatar = setting_module.GetDefaultDisableGravatar()
disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)}
} else if err != nil {
return err return err
} else {
disableGravatar = disableGravatarSetting.GetValueBool()
} }
var enableFederatedAvatar bool enableFederatedAvatar, err := GetSettingBool(ctx, KeyPictureEnableFederatedAvatar, setting_module.GetDefaultEnableFederatedAvatar(disableGravatar))
enableFederatedAvatarSetting, err := GetSetting(ctx, KeyPictureEnableFederatedAvatar) if err != nil {
if IsErrSettingIsNotExist(err) {
enableFederatedAvatar = setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)
enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)}
} else if err != nil {
return err return err
} else {
enableFederatedAvatar = disableGravatarSetting.GetValueBool()
} }
if setting_module.OfflineMode { if setting_module.OfflineMode {
disableGravatar = true if !disableGravatar {
enableFederatedAvatar = false
if !GetSettingBool(ctx, KeyPictureDisableGravatar) {
if err := SetSettingNoVersion(ctx, KeyPictureDisableGravatar, "true"); err != nil { if err := SetSettingNoVersion(ctx, KeyPictureDisableGravatar, "true"); err != nil {
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureDisableGravatar, err) return fmt.Errorf("failed to set setting %q: %w", KeyPictureDisableGravatar, err)
} }
} }
if GetSettingBool(ctx, KeyPictureEnableFederatedAvatar) { disableGravatar = true
if enableFederatedAvatar {
if err := SetSettingNoVersion(ctx, KeyPictureEnableFederatedAvatar, "false"); err != nil { if err := SetSettingNoVersion(ctx, KeyPictureEnableFederatedAvatar, "false"); err != nil {
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err) return fmt.Errorf("failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err)
} }
} }
enableFederatedAvatar = false
} }
if enableFederatedAvatar || !disableGravatar { if enableFederatedAvatar || !disableGravatar {
var err error var err error
GravatarSourceURL, err = url.Parse(setting_module.GravatarSource) GravatarSourceURL, err = url.Parse(setting_module.GravatarSource)
if err != nil { if err != nil {
return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err) return fmt.Errorf("failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err)
} }
} }
if GravatarSourceURL != nil && enableFederatedAvatarSetting.GetValueBool() { if GravatarSourceURL != nil && enableFederatedAvatar {
LibravatarService = libravatar.New() LibravatarService = libravatar.New()
if GravatarSourceURL.Scheme == "https" { if GravatarSourceURL.Scheme == "https" {
LibravatarService.SetUseHTTPS(true) LibravatarService.SetUseHTTPS(true)

View File

@ -67,7 +67,9 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
useLocalAvatar := false useLocalAvatar := false
autoGenerateAvatar := false autoGenerateAvatar := false
disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar) disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar,
setting.GetDefaultDisableGravatar(),
)
switch { switch {
case u.UseCustomAvatar: case u.UseCustomAvatar:

View File

@ -10,6 +10,8 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
_ "code.gitea.io/gitea/models/user" _ "code.gitea.io/gitea/models/user"
) )

View File

@ -15,8 +15,6 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
_ "code.gitea.io/gitea/models" // https://discourse.gitea.io/t/testfixtures-could-not-clean-table-access-no-such-table-access/4137/4
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View File

@ -8,6 +8,10 @@ import (
"testing" "testing"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@ -250,6 +250,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
return return
} }
} }
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages)

View File

@ -471,6 +471,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
} }
ctx.Repo.Owner = owner ctx.Repo.Owner = owner
ctx.ContextUser = owner ctx.ContextUser = owner
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Username"] = ctx.Repo.Owner.Name
// redirect link to wiki // redirect link to wiki

View File

@ -221,8 +221,18 @@ type RunOpts struct {
Dir string Dir string
Stdout, Stderr io.Writer Stdout, Stderr io.Writer
Stdin io.Reader
PipelineFunc func(context.Context, context.CancelFunc) error // Stdin is used for passing input to the command
// The caller must make sure the Stdin writer is closed properly to finish the Run function.
// Otherwise, the Run function may hang for long time or forever, especially when the Git's context deadline is not the same as the caller's.
// Some common mistakes:
// * `defer stdinWriter.Close()` then call `cmd.Run()`: the Run() would never return if the command is killed by timeout
// * `go { case <- parentContext.Done(): stdinWriter.Close() }` with `cmd.Run(DefaultTimeout)`: the command would have been killed by timeout but the Run doesn't return until stdinWriter.Close()
// * `go { if stdoutReader.Read() err != nil: stdinWriter.Close() }` with `cmd.Run()`: the stdoutReader may never return error if the command is killed by timeout
// In the future, ideally the git module itself should have full control of the stdin, to avoid such problems and make it easier to refactor to a better architecture.
Stdin io.Reader
PipelineFunc func(context.Context, context.CancelFunc) error
} }
func commonBaseEnvs() []string { func commonBaseEnvs() []string {

View File

@ -7,6 +7,7 @@ import (
"context" "context"
"os" "os"
"runtime/pprof" "runtime/pprof"
"slices"
"sync/atomic" "sync/atomic"
"time" "time"
@ -20,7 +21,6 @@ import (
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
) )
var ( var (
@ -54,22 +54,22 @@ func index(ctx context.Context, indexer internal.Indexer, repoID int64) error {
} }
// skip forks from being indexed if unit is not present // skip forks from being indexed if unit is not present
if !util.SliceContains(repoTypes, "forks") && repo.IsFork { if !slices.Contains(repoTypes, "forks") && repo.IsFork {
return nil return nil
} }
// skip mirrors from being indexed if unit is not present // skip mirrors from being indexed if unit is not present
if !util.SliceContains(repoTypes, "mirrors") && repo.IsMirror { if !slices.Contains(repoTypes, "mirrors") && repo.IsMirror {
return nil return nil
} }
// skip templates from being indexed if unit is not present // skip templates from being indexed if unit is not present
if !util.SliceContains(repoTypes, "templates") && repo.IsTemplate { if !slices.Contains(repoTypes, "templates") && repo.IsTemplate {
return nil return nil
} }
// skip regular repos from being indexed if unit is not present // skip regular repos from being indexed if unit is not present
if !util.SliceContains(repoTypes, "sources") && !repo.IsFork && !repo.IsMirror && !repo.IsTemplate { if !slices.Contains(repoTypes, "sources") && !repo.IsFork && !repo.IsMirror && !repo.IsTemplate {
return nil return nil
} }
@ -122,21 +122,6 @@ func Init() {
indexer := *globalIndexer.Load() indexer := *globalIndexer.Load()
for _, indexerData := range items { for _, indexerData := range items {
log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) log.Trace("IndexerData Process Repo: %d", indexerData.RepoID)
// FIXME: it seems there is a bug in `CatFileBatch` or `nio.Pipe`, which will cause the process to hang forever in rare cases
/*
sync.(*Cond).Wait(cond.go:70)
github.com/djherbis/nio/v3.(*PipeReader).Read(sync.go:106)
bufio.(*Reader).fill(bufio.go:106)
bufio.(*Reader).ReadSlice(bufio.go:372)
bufio.(*Reader).collectFragments(bufio.go:447)
bufio.(*Reader).ReadString(bufio.go:494)
code.gitea.io/gitea/modules/git.ReadBatchLine(batch_reader.go:149)
code.gitea.io/gitea/modules/indexer/code.(*BleveIndexer).addUpdate(bleve.go:214)
code.gitea.io/gitea/modules/indexer/code.(*BleveIndexer).Index(bleve.go:296)
code.gitea.io/gitea/modules/indexer/code.(*wrappedIndexer).Index(wrapped.go:74)
code.gitea.io/gitea/modules/indexer/code.index(indexer.go:105)
*/
if err := index(ctx, indexer, indexerData.RepoID); err != nil { if err := index(ctx, indexer, indexerData.RepoID); err != nil {
unhandled = append(unhandled, indexerData) unhandled = append(unhandled, indexerData)
if !setting.IsInTesting { if !setting.IsInTesting {

View File

@ -16,6 +16,8 @@ import (
"code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/indexer/code/internal"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View File

@ -15,6 +15,8 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View File

@ -10,6 +10,7 @@ package tests
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"testing" "testing"
"time" "time"
@ -457,7 +458,7 @@ var cases = []*testIndexerCase{
assert.Contains(t, data[v.ID].MentionIDs, int64(1)) assert.Contains(t, data[v.ID].MentionIDs, int64(1))
} }
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return util.SliceContains(v.MentionIDs, 1) return slices.Contains(v.MentionIDs, 1)
}), result.Total) }), result.Total)
}, },
}, },
@ -478,7 +479,7 @@ var cases = []*testIndexerCase{
assert.Contains(t, data[v.ID].ReviewedIDs, int64(1)) assert.Contains(t, data[v.ID].ReviewedIDs, int64(1))
} }
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return util.SliceContains(v.ReviewedIDs, 1) return slices.Contains(v.ReviewedIDs, 1)
}), result.Total) }), result.Total)
}, },
}, },
@ -499,7 +500,7 @@ var cases = []*testIndexerCase{
assert.Contains(t, data[v.ID].ReviewRequestedIDs, int64(1)) assert.Contains(t, data[v.ID].ReviewRequestedIDs, int64(1))
} }
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return util.SliceContains(v.ReviewRequestedIDs, 1) return slices.Contains(v.ReviewRequestedIDs, 1)
}), result.Total) }), result.Total)
}, },
}, },
@ -520,7 +521,7 @@ var cases = []*testIndexerCase{
assert.Contains(t, data[v.ID].SubscriberIDs, int64(1)) assert.Contains(t, data[v.ID].SubscriberIDs, int64(1))
} }
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return util.SliceContains(v.SubscriberIDs, 1) return slices.Contains(v.SubscriberIDs, 1)
}), result.Total) }), result.Total)
}, },
}, },

View File

@ -16,6 +16,8 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/activities"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View File

@ -1,27 +1,62 @@
// Copyright 2023 The Gitea Authors. All rights reserved. // Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Package queue implements a specialized queue system for Gitea. // Package queue implements a specialized concurrent queue system for Gitea.
// //
// There are two major kinds of concepts: // Terminology:
// //
// * The "base queue": channel, level, redis: // 1. Item:
// - They have the same abstraction, the same interface, and they are tested by the same testing code. // - An item can be a simple value, such as an integer, or a more complex structure that has multiple fields.
// - The dummy(immediate) queue is special, it's not a real queue, it's only used as a no-op queue or a testing queue. // Usually a item serves as a task or a message. Sets of items will be sent to a queue handler to be processed.
// - It's represented as a JSON-marshaled binary slice in the queue
// //
// * The WorkerPoolQueue: it uses the "base queue" to provide "worker pool" function. // 2. Batch:
// - It calls the "handler" to process the data in the base queue. // - A collection of items that are grouped together for processing. Each worker receives a batch of items.
// - Its "Push" function doesn't block forever, //
// it will return an error if the queue is full after the timeout. // 3. Worker:
// - Individual unit of execution designed to process items from the queue. It's a goroutine that calls the Handler.
// - Workers will get new items through a channel (WorkerPoolQueue is responsible for the distribution).
// - Workers operate in parallel. The default value of max workers is determined by the setting system.
//
// 4. Handler (represented by HandlerFuncT type):
// - It's the function responsible for processing items. Each active worker will call it.
// - If an item or some items are not psuccessfully rocessed, the handler could return them as "unhandled items".
// In such scenarios, the queue system ensures these unhandled items are returned to the base queue after a brief delay.
// This mechanism is particularly beneficial in cases where the processing entity (like a document indexer) is
// temporarily unavailable. It ensures that no item is skipped or lost due to transient failures in the processing
// mechanism.
//
// 5. Base queue:
// - Represents the underlying storage mechanism for the queue. There are several implementations:
// - Channel: Uses Go's native channel constructs to manage the queue, suitable for in-memory queuing.
// - LevelDB: Especially useful in persistent queues for single instances.
// - Redis: Suitable for clusters, where we may have multiple nodes.
// - Dummy: This is special, it's not a real queue, it's a immediate no-op queue, which is useful for tests.
// - They all have the same abstraction, the same interface, and they are tested by the same testing code.
//
// 6. WorkerPoolQueue:
// - It's responsible to glue all together, using the "base queue" to provide "worker pool" functionality. It creates
// new workers if needed and can flush the queue, running all the items synchronously till it finishes.
// - Its "Push" function doesn't block forever, it will return an error if the queue is full after the timeout.
//
// 7. Manager:
// - The purpose of it is to serve as a centralized manager for multiple WorkerPoolQueue instances. Whenever we want
// to create a new queue, flush, or get a specific queue, we could use it.
// //
// A queue can be "simple" or "unique". A unique queue will try to avoid duplicate items. // A queue can be "simple" or "unique". A unique queue will try to avoid duplicate items.
// Unique queue's "Has" function can be used to check whether an item is already in the queue, // Unique queue's "Has" function can be used to check whether an item is already in the queue,
// although it's not 100% reliable due to there is no proper transaction support. // although it's not 100% reliable due to the lack of proper transaction support.
// Simple queue's "Has" function always returns "has=false". // Simple queue's "Has" function always returns "has=false".
// //
// The HandlerFuncT function is called by the WorkerPoolQueue to process the data in the base queue. // A WorkerPoolQueue is a generic struct; this means it will work with any type but just for that type.
// If the handler returns "unhandled" items, they will be re-queued to the base queue after a slight delay, // If you want another kind of items to run, you would have to call the manager to create a new WorkerPoolQueue for you
// in case the item processor (eg: document indexer) is not available. // with a different handler that works with this new type of item. As an example of this:
//
// func Init() error {
// itemQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "queue-name", handler)
// ...
// }
// func handler(items ...*mypkg.QueueItem) []*mypkg.QueueItem { ... }
package queue package queue
import "code.gitea.io/gitea/modules/util" import "code.gitea.io/gitea/modules/util"

View File

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/avatars" "code.gitea.io/gitea/models/avatars"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -34,42 +35,36 @@ type PushCommits struct {
HeadCommit *PushCommit HeadCommit *PushCommit
CompareURL string CompareURL string
Len int Len int
avatars map[string]string
emailUsers map[string]*user_model.User
} }
// NewPushCommits creates a new PushCommits object. // NewPushCommits creates a new PushCommits object.
func NewPushCommits() *PushCommits { func NewPushCommits() *PushCommits {
return &PushCommits{ return &PushCommits{}
avatars: make(map[string]string),
emailUsers: make(map[string]*user_model.User),
}
} }
// toAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object. // toAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object.
func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) { func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.User, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) {
var err error var err error
authorUsername := "" authorUsername := ""
author, ok := pc.emailUsers[commit.AuthorEmail] author, ok := emailUsers[commit.AuthorEmail]
if !ok { if !ok {
author, err = user_model.GetUserByEmail(ctx, commit.AuthorEmail) author, err = user_model.GetUserByEmail(ctx, commit.AuthorEmail)
if err == nil { if err == nil {
authorUsername = author.Name authorUsername = author.Name
pc.emailUsers[commit.AuthorEmail] = author emailUsers[commit.AuthorEmail] = author
} }
} else { } else {
authorUsername = author.Name authorUsername = author.Name
} }
committerUsername := "" committerUsername := ""
committer, ok := pc.emailUsers[commit.CommitterEmail] committer, ok := emailUsers[commit.CommitterEmail]
if !ok { if !ok {
committer, err = user_model.GetUserByEmail(ctx, commit.CommitterEmail) committer, err = user_model.GetUserByEmail(ctx, commit.CommitterEmail)
if err == nil { if err == nil {
// TODO: check errors other than email not found. // TODO: check errors other than email not found.
committerUsername = committer.Name committerUsername = committer.Name
pc.emailUsers[commit.CommitterEmail] = committer emailUsers[commit.CommitterEmail] = committer
} }
} else { } else {
committerUsername = committer.Name committerUsername = committer.Name
@ -107,11 +102,10 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
commits := make([]*api.PayloadCommit, len(pc.Commits)) commits := make([]*api.PayloadCommit, len(pc.Commits))
var headCommit *api.PayloadCommit var headCommit *api.PayloadCommit
if pc.emailUsers == nil { emailUsers := make(map[string]*user_model.User)
pc.emailUsers = make(map[string]*user_model.User)
}
for i, commit := range pc.Commits { for i, commit := range pc.Commits {
apiCommit, err := pc.toAPIPayloadCommit(ctx, repoPath, repoLink, commit) apiCommit, err := pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, commit)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -123,7 +117,7 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
} }
if pc.HeadCommit != nil && headCommit == nil { if pc.HeadCommit != nil && headCommit == nil {
var err error var err error
headCommit, err = pc.toAPIPayloadCommit(ctx, repoPath, repoLink, pc.HeadCommit) headCommit, err = pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, pc.HeadCommit)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -134,35 +128,21 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
// AvatarLink tries to match user in database with e-mail // AvatarLink tries to match user in database with e-mail
// in order to show custom avatar, and falls back to general avatar link. // in order to show custom avatar, and falls back to general avatar link.
func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string { func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string {
if pc.avatars == nil {
pc.avatars = make(map[string]string)
}
avatar, ok := pc.avatars[email]
if ok {
return avatar
}
size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor
u, ok := pc.emailUsers[email] v, _ := cache.GetWithContextCache(ctx, "push_commits", email, func() (string, error) {
if !ok { u, err := user_model.GetUserByEmail(ctx, email)
var err error
u, err = user_model.GetUserByEmail(ctx, email)
if err != nil { if err != nil {
pc.avatars[email] = avatars.GenerateEmailAvatarFastLink(ctx, email, size)
if !user_model.IsErrUserNotExist(err) { if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err) log.Error("GetUserByEmail: %v", err)
return "" return "", err
} }
} else { return avatars.GenerateEmailAvatarFastLink(ctx, email, size), nil
pc.emailUsers[email] = u
} }
} return u.AvatarLinkWithSize(ctx, size), nil
if u != nil { })
pc.avatars[email] = u.AvatarLinkWithSize(ctx, size)
}
return pc.avatars[email] return v
} }
// CommitToPushCommit transforms a git.Commit to PushCommit type. // CommitToPushCommit transforms a git.Commit to PushCommit type.
@ -189,7 +169,5 @@ func GitToPushCommits(gitCommits []*git.Commit) *PushCommits {
HeadCommit: nil, HeadCommit: nil,
CompareURL: "", CompareURL: "",
Len: len(commits), Len: len(commits),
avatars: make(map[string]string),
emailUsers: make(map[string]*user_model.User),
} }
} }

View File

@ -103,11 +103,9 @@ func TestPushCommits_ToAPIPayloadCommits(t *testing.T) {
assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified) assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified)
} }
func enableGravatar(t *testing.T) { func initGravatarSource(t *testing.T) {
err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "false")
assert.NoError(t, err)
setting.GravatarSource = "https://secure.gravatar.com/avatar" setting.GravatarSource = "https://secure.gravatar.com/avatar"
err = system_model.Init(db.DefaultContext) err := system_model.Init(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -134,7 +132,7 @@ func TestPushCommits_AvatarLink(t *testing.T) {
}, },
} }
enableGravatar(t) initGravatarSource(t)
assert.Equal(t, assert.Equal(t,
"https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor),

View File

@ -22,7 +22,6 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues" issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -156,142 +155,6 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
return nil return nil
} }
// CreateRepoOptions contains the create repository options
type CreateRepoOptions struct {
Name string
Description string
OriginalURL string
GitServiceType api.GitServiceType
Gitignores string
IssueLabels string
License string
Readme string
DefaultBranch string
IsPrivate bool
IsMirror bool
IsTemplate bool
AutoInit bool
Status repo_model.RepositoryStatus
TrustModel repo_model.TrustModelType
MirrorInterval string
}
// CreateRepository creates a repository for the user/organization.
func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
if !doer.IsAdmin && !u.CanCreateRepo() {
return nil, repo_model.ErrReachLimitOfRepo{
Limit: u.MaxRepoCreation,
}
}
if len(opts.DefaultBranch) == 0 {
opts.DefaultBranch = setting.Repository.DefaultBranch
}
// Check if label template exist
if len(opts.IssueLabels) > 0 {
if _, err := LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil {
return nil, err
}
}
repo := &repo_model.Repository{
OwnerID: u.ID,
Owner: u,
OwnerName: u.Name,
Name: opts.Name,
LowerName: strings.ToLower(opts.Name),
Description: opts.Description,
OriginalURL: opts.OriginalURL,
OriginalServiceType: opts.GitServiceType,
IsPrivate: opts.IsPrivate,
IsFsckEnabled: !opts.IsMirror,
IsTemplate: opts.IsTemplate,
CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
Status: opts.Status,
IsEmpty: !opts.AutoInit,
TrustModel: opts.TrustModel,
IsMirror: opts.IsMirror,
DefaultBranch: opts.DefaultBranch,
}
var rollbackRepo *repo_model.Repository
if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error {
if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
return err
}
// No need for init mirror.
if opts.IsMirror {
return nil
}
repoPath := repo_model.RepoPath(u.Name, repo.Name)
isExist, err := util.IsExist(repoPath)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
return err
}
if isExist {
// repo already exists - We have two or three options.
// 1. We fail stating that the directory exists
// 2. We create the db repository to go with this data and adopt the git repo
// 3. We delete it and start afresh
//
// Previously Gitea would just delete and start afresh - this was naughty.
// So we will now fail and delegate to other functionality to adopt or delete
log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
return repo_model.ErrRepoFilesAlreadyExist{
Uname: u.Name,
Name: repo.Name,
}
}
if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil {
if err2 := util.RemoveAll(repoPath); err2 != nil {
log.Error("initRepository: %v", err)
return fmt.Errorf(
"delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2)
}
return fmt.Errorf("initRepository: %w", err)
}
// Initialize Issue Labels if selected
if len(opts.IssueLabels) > 0 {
if err = InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
rollbackRepo = repo
rollbackRepo.OwnerID = u.ID
return fmt.Errorf("InitializeLabels: %w", err)
}
}
if err := CheckDaemonExportOK(ctx, repo); err != nil {
return fmt.Errorf("checkDaemonExportOK: %w", err)
}
if stdout, _, err := git.NewCommand(ctx, "update-server-info").
SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
rollbackRepo = repo
rollbackRepo.OwnerID = u.ID
return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
}
return nil
}); err != nil {
if rollbackRepo != nil {
if errDelete := models.DeleteRepository(doer, rollbackRepo.OwnerID, rollbackRepo.ID); errDelete != nil {
log.Error("Rollback deleteRepository: %v", errDelete)
}
}
return nil, err
}
return repo, nil
}
const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular
// getDirectorySize returns the disk consumption for a given path // getDirectorySize returns the disk consumption for a given path

View File

@ -4,151 +4,16 @@
package repository package repository
import ( import (
"fmt"
"testing" "testing"
"code.gitea.io/gitea/models"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestIncludesAllRepositoriesTeams(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
testTeamRepositories := func(teamID int64, repoIds []int64) {
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name)
assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name)
assert.Len(t, team.Repos, len(repoIds), "%s: repo count", team.Name)
for i, rid := range repoIds {
if rid > 0 {
assert.True(t, models.HasRepository(team, rid), "%s: HasRepository(%d) %d", rid, i)
}
}
}
// Get an admin user.
user, err := user_model.GetUserByID(db.DefaultContext, 1)
assert.NoError(t, err, "GetUserByID")
// Create org.
org := &organization.Organization{
Name: "All_repo",
IsActive: true,
Type: user_model.UserTypeOrganization,
Visibility: structs.VisibleTypePublic,
}
assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization")
// Check Owner team.
ownerTeam, err := org.GetOwnerTeam(db.DefaultContext)
assert.NoError(t, err, "GetOwnerTeam")
assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
// Create repos.
repoIds := make([]int64, 0)
for i := 0; i < 3; i++ {
r, err := CreateRepository(user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)})
assert.NoError(t, err, "CreateRepository %d", i)
if r != nil {
repoIds = append(repoIds, r.ID)
}
}
// Get fresh copy of Owner team after creating repos.
ownerTeam, err = org.GetOwnerTeam(db.DefaultContext)
assert.NoError(t, err, "GetOwnerTeam")
// Create teams and check repositories.
teams := []*organization.Team{
ownerTeam,
{
OrgID: org.ID,
Name: "team one",
AccessMode: perm.AccessModeRead,
IncludesAllRepositories: true,
},
{
OrgID: org.ID,
Name: "team 2",
AccessMode: perm.AccessModeRead,
IncludesAllRepositories: false,
},
{
OrgID: org.ID,
Name: "team three",
AccessMode: perm.AccessModeWrite,
IncludesAllRepositories: true,
},
{
OrgID: org.ID,
Name: "team 4",
AccessMode: perm.AccessModeWrite,
IncludesAllRepositories: false,
},
}
teamRepos := [][]int64{
repoIds,
repoIds,
{},
repoIds,
{},
}
for i, team := range teams {
if i > 0 { // first team is Owner.
assert.NoError(t, models.NewTeam(team), "%s: NewTeam", team.Name)
}
testTeamRepositories(team.ID, teamRepos[i])
}
// Update teams and check repositories.
teams[3].IncludesAllRepositories = false
teams[4].IncludesAllRepositories = true
teamRepos[4] = repoIds
for i, team := range teams {
assert.NoError(t, models.UpdateTeam(team, false, true), "%s: UpdateTeam", team.Name)
testTeamRepositories(team.ID, teamRepos[i])
}
// Create repo and check teams repositories.
r, err := CreateRepository(user, org.AsUser(), CreateRepoOptions{Name: "repo-last"})
assert.NoError(t, err, "CreateRepository last")
if r != nil {
repoIds = append(repoIds, r.ID)
}
teamRepos[0] = repoIds
teamRepos[1] = repoIds
teamRepos[4] = repoIds
for i, team := range teams {
testTeamRepositories(team.ID, teamRepos[i])
}
// Remove repo and check teams repositories.
assert.NoError(t, models.DeleteRepository(user, org.ID, repoIds[0]), "DeleteRepository")
teamRepos[0] = repoIds[1:]
teamRepos[1] = repoIds[1:]
teamRepos[3] = repoIds[1:3]
teamRepos[4] = repoIds[1:]
for i, team := range teams {
testTeamRepositories(team.ID, teamRepos[i])
}
// Wipe created items.
for i, rid := range repoIds {
if i > 0 { // first repo already deleted.
assert.NoError(t, models.DeleteRepository(user, org.ID, rid), "DeleteRepository %d", i)
}
}
assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization")
}
func TestUpdateRepositoryVisibilityChanged(t *testing.T) { func TestUpdateRepositoryVisibilityChanged(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View File

@ -241,7 +241,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
defaultBranch = templateRepo.DefaultBranch defaultBranch = templateRepo.DefaultBranch
} }
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch) return InitRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
} }
func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) { func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) {
@ -356,7 +356,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
} }
} }
if err = checkInitRepository(ctx, owner.Name, generateRepo.Name); err != nil { if err = CheckInitRepository(ctx, owner.Name, generateRepo.Name); err != nil {
return generateRepo, err return generateRepo, err
} }

View File

@ -108,12 +108,7 @@ done
} }
// CreateDelegateHooks creates all the hooks scripts for the repo // CreateDelegateHooks creates all the hooks scripts for the repo
func CreateDelegateHooks(repoPath string) error { func CreateDelegateHooks(repoPath string) (err error) {
return createDelegateHooks(repoPath)
}
// createDelegateHooks creates all the hooks scripts for the repo
func createDelegateHooks(repoPath string) (err error) {
hookNames, hookTpls, giteaHookTpls := getHookTemplates() hookNames, hookTpls, giteaHookTpls := getHookTemplates()
hookDir := filepath.Join(repoPath, "hooks") hookDir := filepath.Join(repoPath, "hooks")

View File

@ -4,7 +4,6 @@
package repository package repository
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"os" "os"
@ -21,7 +20,6 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates/vars"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
) )
@ -126,95 +124,8 @@ func LoadRepoConfig() error {
return nil return nil
} }
func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { // InitRepoCommit temporarily changes with work directory.
commitTimeStr := time.Now().Format(time.RFC3339) func InitRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
authorSig := repo.Owner.NewGitSig()
// Because this may call hooks we should pass in the environment
env := append(os.Environ(),
"GIT_AUTHOR_NAME="+authorSig.Name,
"GIT_AUTHOR_EMAIL="+authorSig.Email,
"GIT_AUTHOR_DATE="+commitTimeStr,
"GIT_COMMITTER_NAME="+authorSig.Name,
"GIT_COMMITTER_EMAIL="+authorSig.Email,
"GIT_COMMITTER_DATE="+commitTimeStr,
)
// Clone to temporary path and do the init commit.
if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir).
SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)).
RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil {
log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err)
return fmt.Errorf("git clone: %w", err)
}
// README
data, err := options.Readme(opts.Readme)
if err != nil {
return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err)
}
cloneLink := repo.CloneLink()
match := map[string]string{
"Name": repo.Name,
"Description": repo.Description,
"CloneURL.SSH": cloneLink.SSH,
"CloneURL.HTTPS": cloneLink.HTTPS,
"OwnerName": repo.OwnerName,
}
res, err := vars.Expand(string(data), match)
if err != nil {
// here we could just log the error and continue the rendering
log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err)
}
if err = os.WriteFile(filepath.Join(tmpDir, "README.md"),
[]byte(res), 0o644); err != nil {
return fmt.Errorf("write README.md: %w", err)
}
// .gitignore
if len(opts.Gitignores) > 0 {
var buf bytes.Buffer
names := strings.Split(opts.Gitignores, ",")
for _, name := range names {
data, err = options.Gitignore(name)
if err != nil {
return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err)
}
buf.WriteString("# ---> " + name + "\n")
buf.Write(data)
buf.WriteString("\n")
}
if buf.Len() > 0 {
if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil {
return fmt.Errorf("write .gitignore: %w", err)
}
}
}
// LICENSE
if len(opts.License) > 0 {
data, err = getLicense(opts.License, &licenseValues{
Owner: repo.OwnerName,
Email: authorSig.Email,
Repo: repo.Name,
Year: time.Now().Format("2006"),
})
if err != nil {
return fmt.Errorf("getLicense[%s]: %w", opts.License, err)
}
if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil {
return fmt.Errorf("write LICENSE: %w", err)
}
}
return nil
}
// initRepoCommit temporarily changes with work directory.
func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
commitTimeStr := time.Now().Format(time.RFC3339) commitTimeStr := time.Now().Format(time.RFC3339)
sig := u.NewGitSig() sig := u.NewGitSig()
@ -277,7 +188,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi
return nil return nil
} }
func checkInitRepository(ctx context.Context, owner, name string) (err error) { func CheckInitRepository(ctx context.Context, owner, name string) (err error) {
// Somehow the directory could exist. // Somehow the directory could exist.
repoPath := repo_model.RepoPath(owner, name) repoPath := repo_model.RepoPath(owner, name)
isExist, err := util.IsExist(repoPath) isExist, err := util.IsExist(repoPath)
@ -295,77 +206,12 @@ func checkInitRepository(ctx context.Context, owner, name string) (err error) {
// Init git bare new repository. // Init git bare new repository.
if err = git.InitRepository(ctx, repoPath, true); err != nil { if err = git.InitRepository(ctx, repoPath, true); err != nil {
return fmt.Errorf("git.InitRepository: %w", err) return fmt.Errorf("git.InitRepository: %w", err)
} else if err = createDelegateHooks(repoPath); err != nil { } else if err = CreateDelegateHooks(repoPath); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err) return fmt.Errorf("createDelegateHooks: %w", err)
} }
return nil return nil
} }
// InitRepository initializes README and .gitignore if needed.
func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) {
if err = checkInitRepository(ctx, repo.OwnerName, repo.Name); err != nil {
return err
}
// Initialize repository according to user's choice.
if opts.AutoInit {
tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
if err != nil {
return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err)
}
defer func() {
if err := util.RemoveAll(tmpDir); err != nil {
log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err)
}
}()
if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil {
return fmt.Errorf("prepareRepoCommit: %w", err)
}
// Apply changes and commit.
if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
return fmt.Errorf("initRepoCommit: %w", err)
}
}
// Re-fetch the repository from database before updating it (else it would
// override changes that were done earlier with sql)
if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
return fmt.Errorf("getRepositoryByID: %w", err)
}
if !opts.AutoInit {
repo.IsEmpty = true
}
repo.DefaultBranch = setting.Repository.DefaultBranch
if len(opts.DefaultBranch) > 0 {
repo.DefaultBranch = opts.DefaultBranch
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
return fmt.Errorf("openRepository: %w", err)
}
defer gitRepo.Close()
if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
return fmt.Errorf("setDefaultBranch: %w", err)
}
if !repo.IsEmpty {
if _, err := SyncRepoBranches(ctx, repo.ID, u.ID); err != nil {
return fmt.Errorf("SyncRepoBranches: %w", err)
}
}
}
if err = UpdateRepository(ctx, repo, false); err != nil {
return fmt.Errorf("updateRepository: %w", err)
}
return nil
}
// InitializeLabels adds a label set to a repository using a template // InitializeLabels adds a label set to a repository using a template
func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error { func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error {
list, err := LoadTemplateLabelsByDisplayName(labelTemplate) list, err := LoadTemplateLabelsByDisplayName(labelTemplate)

View File

@ -13,14 +13,14 @@ import (
"code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/options"
) )
type licenseValues struct { type LicenseValues struct {
Owner string Owner string
Email string Email string
Repo string Repo string
Year string Year string
} }
func getLicense(name string, values *licenseValues) ([]byte, error) { func GetLicense(name string, values *LicenseValues) ([]byte, error) {
data, err := options.License(name) data, err := options.License(name)
if err != nil { if err != nil {
return nil, fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) return nil, fmt.Errorf("GetRepoInitFile[%s]: %w", name, err)
@ -28,7 +28,7 @@ func getLicense(name string, values *licenseValues) ([]byte, error) {
return fillLicensePlaceholder(name, values, data), nil return fillLicensePlaceholder(name, values, data), nil
} }
func fillLicensePlaceholder(name string, values *licenseValues, origin []byte) []byte { func fillLicensePlaceholder(name string, values *LicenseValues, origin []byte) []byte {
placeholder := getLicensePlaceholder(name) placeholder := getLicensePlaceholder(name)
scanner := bufio.NewScanner(bytes.NewReader(origin)) scanner := bufio.NewScanner(bytes.NewReader(origin))

View File

@ -13,7 +13,7 @@ import (
func Test_getLicense(t *testing.T) { func Test_getLicense(t *testing.T) {
type args struct { type args struct {
name string name string
values *licenseValues values *LicenseValues
} }
tests := []struct { tests := []struct {
name string name string
@ -25,7 +25,7 @@ func Test_getLicense(t *testing.T) {
name: "regular", name: "regular",
args: args{ args: args{
name: "MIT", name: "MIT",
values: &licenseValues{Owner: "Gitea", Year: "2023"}, values: &LicenseValues{Owner: "Gitea", Year: "2023"},
}, },
want: `MIT License want: `MIT License
@ -49,11 +49,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := getLicense(tt.args.name, tt.args.values) got, err := GetLicense(tt.args.name, tt.args.values)
if !tt.wantErr(t, err, fmt.Sprintf("getLicense(%v, %v)", tt.args.name, tt.args.values)) { if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) {
return return
} }
assert.Equalf(t, tt.want, string(got), "getLicense(%v, %v)", tt.args.name, tt.args.values) assert.Equalf(t, tt.want, string(got), "GetLicense(%v, %v)", tt.args.name, tt.args.values)
}) })
} }
} }
@ -61,7 +61,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
func Test_fillLicensePlaceholder(t *testing.T) { func Test_fillLicensePlaceholder(t *testing.T) {
type args struct { type args struct {
name string name string
values *licenseValues values *LicenseValues
origin string origin string
} }
tests := []struct { tests := []struct {
@ -73,7 +73,7 @@ func Test_fillLicensePlaceholder(t *testing.T) {
name: "owner", name: "owner",
args: args{ args: args{
name: "regular", name: "regular",
values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: ` origin: `
<name of author> <name of author>
<owner> <owner>
@ -104,7 +104,7 @@ Gitea
name: "email", name: "email",
args: args{ args: args{
name: "regular", name: "regular",
values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: ` origin: `
[EMAIL] [EMAIL]
`, `,
@ -117,7 +117,7 @@ teabot@gitea.io
name: "repo", name: "repo",
args: args{ args: args{
name: "regular", name: "regular",
values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: ` origin: `
<program> <program>
<one line to give the program's name and a brief idea of what it does.> <one line to give the program's name and a brief idea of what it does.>
@ -132,7 +132,7 @@ gitea
name: "year", name: "year",
args: args{ args: args{
name: "regular", name: "regular",
values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: ` origin: `
<year> <year>
[YEAR] [YEAR]
@ -155,7 +155,7 @@ gitea
name: "0BSD", name: "0BSD",
args: args{ args: args{
name: "0BSD", name: "0BSD",
values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"},
origin: ` origin: `
Copyright (C) YEAR by AUTHOR EMAIL Copyright (C) YEAR by AUTHOR EMAIL

View File

@ -8,6 +8,8 @@ import (
"testing" "testing"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
_ "code.gitea.io/gitea/models/actions"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@ -256,11 +256,11 @@ func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) {
repoPath := repo.RepoPath() repoPath := repo.RepoPath()
if err := createDelegateHooks(repoPath); err != nil { if err := CreateDelegateHooks(repoPath); err != nil {
return repo, fmt.Errorf("createDelegateHooks: %w", err) return repo, fmt.Errorf("createDelegateHooks: %w", err)
} }
if repo.HasWiki() { if repo.HasWiki() {
if err := createDelegateHooks(repo.WikiPath()); err != nil { if err := CreateDelegateHooks(repo.WikiPath()); err != nil {
return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err)
} }
} }

View File

@ -13,10 +13,11 @@ import (
// Actions settings // Actions settings
var ( var (
Actions = struct { Actions = struct {
LogStorage *Storage // how the created logs should be stored LogStorage *Storage // how the created logs should be stored
ArtifactStorage *Storage // how the created artifacts should be stored ArtifactStorage *Storage // how the created artifacts should be stored
Enabled bool ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"`
DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` Enabled bool
DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"`
}{ }{
Enabled: false, Enabled: false,
DefaultActionsURL: defaultActionsURLGitHub, DefaultActionsURL: defaultActionsURLGitHub,
@ -76,5 +77,10 @@ func loadActionsFrom(rootCfg ConfigProvider) error {
Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec) Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec)
// default to 90 days in Github Actions
if Actions.ArtifactRetentionDays <= 0 {
Actions.ArtifactRetentionDays = 90
}
return err return err
} }

View File

@ -174,9 +174,16 @@ func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
return sections return sections
} }
func configProviderLoadOptions() ini.LoadOptions {
return ini.LoadOptions{
KeyValueDelimiterOnWrite: " = ",
IgnoreContinuation: true,
}
}
// NewConfigProviderFromData this function is mainly for testing purpose // NewConfigProviderFromData this function is mainly for testing purpose
func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
cfg, err := ini.Load(strings.NewReader(configContent)) cfg, err := ini.LoadSources(configProviderLoadOptions(), strings.NewReader(configContent))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -190,7 +197,7 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
// NewConfigProviderFromFile load configuration from file. // NewConfigProviderFromFile load configuration from file.
// NOTE: do not print any log except error. // NOTE: do not print any log except error.
func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) { func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "}) cfg := ini.Empty(configProviderLoadOptions())
loadedFromEmpty := true loadedFromEmpty := true
if file != "" { if file != "" {
@ -339,6 +346,7 @@ func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, erro
iniFile, err := ini.LoadSources(ini.LoadOptions{ iniFile, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true, IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true, UnescapeValueCommentSymbols: true,
IgnoreContinuation: true,
}, source, others...) }, source, others...)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to load locale ini: %w", err) return nil, fmt.Errorf("unable to load locale ini: %w", err)

View File

@ -30,6 +30,16 @@ key = 123
secSub := cfg.Section("foo.bar.xxx") secSub := cfg.Section("foo.bar.xxx")
assert.Equal(t, "123", secSub.Key("key").String()) assert.Equal(t, "123", secSub.Key("key").String())
}) })
t.Run("TrailingSlash", func(t *testing.T) {
cfg, _ := NewConfigProviderFromData(`
[foo]
key = E:\
xxx = yyy
`)
sec := cfg.Section("foo")
assert.Equal(t, "E:\\", sec.Key("key").String())
assert.Equal(t, "yyy", sec.Key("xxx").String())
})
} }
func TestConfigProviderHelper(t *testing.T) { func TestConfigProviderHelper(t *testing.T) {

View File

@ -46,6 +46,7 @@ var Service = struct {
EnableNotifyMail bool EnableNotifyMail bool
EnableBasicAuth bool EnableBasicAuth bool
EnableReverseProxyAuth bool EnableReverseProxyAuth bool
EnableReverseProxyAuthAPI bool
EnableReverseProxyAutoRegister bool EnableReverseProxyAutoRegister bool
EnableReverseProxyEmail bool EnableReverseProxyEmail bool
EnableReverseProxyFullName bool EnableReverseProxyFullName bool
@ -157,6 +158,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool()
Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()
Service.EnableReverseProxyEmail = sec.Key("ENABLE_REVERSE_PROXY_EMAIL").MustBool() Service.EnableReverseProxyEmail = sec.Key("ENABLE_REVERSE_PROXY_EMAIL").MustBool()
Service.EnableReverseProxyFullName = sec.Key("ENABLE_REVERSE_PROXY_FULL_NAME").MustBool() Service.EnableReverseProxyFullName = sec.Key("ENABLE_REVERSE_PROXY_FULL_NAME").MustBool()

View File

@ -50,7 +50,7 @@ func loadSessionFrom(rootCfg ConfigProvider) {
} }
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea") SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(false) SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400) SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400) SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
SessionConfig.Domain = sec.Key("DOMAIN").String() SessionConfig.Domain = sec.Key("DOMAIN").String()

View File

@ -4,11 +4,11 @@
package templates package templates
import ( import (
"slices"
"strings" "strings"
"code.gitea.io/gitea/modules/assetfs" "code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
) )
func AssetFS() *assetfs.LayeredFS { func AssetFS() *assetfs.LayeredFS {
@ -24,7 +24,7 @@ func ListWebTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return util.SliceRemoveAllFunc(files, func(file string) bool { return slices.DeleteFunc(files, func(file string) bool {
return strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl") return strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl")
}), nil }), nil
} }
@ -34,7 +34,7 @@ func ListMailTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return util.SliceRemoveAllFunc(files, func(file string) bool { return slices.DeleteFunc(files, func(file string) bool {
return !strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl") return !strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl")
}), nil }), nil
} }

View File

@ -1,37 +1,21 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Most of the functions in this file can have better implementations with "golang.org/x/exp/slices".
// However, "golang.org/x/exp" is experimental and unreliable, we shouldn't use it.
// So lets waiting for the "slices" has be promoted to the main repository one day.
package util package util
import "strings" import (
"slices"
// SliceContains returns true if the target exists in the slice. "strings"
func SliceContains[T comparable](slice []T, target T) bool { )
return SliceContainsFunc(slice, func(t T) bool { return t == target })
}
// SliceContainsFunc returns true if any element in the slice satisfies the targetFunc.
func SliceContainsFunc[T any](slice []T, targetFunc func(T) bool) bool {
for _, v := range slice {
if targetFunc(v) {
return true
}
}
return false
}
// SliceContainsString sequential searches if string exists in slice. // SliceContainsString sequential searches if string exists in slice.
func SliceContainsString(slice []string, target string, insensitive ...bool) bool { func SliceContainsString(slice []string, target string, insensitive ...bool) bool {
if len(insensitive) != 0 && insensitive[0] { if len(insensitive) != 0 && insensitive[0] {
target = strings.ToLower(target) target = strings.ToLower(target)
return SliceContainsFunc(slice, func(t string) bool { return strings.ToLower(t) == target }) return slices.ContainsFunc(slice, func(t string) bool { return strings.ToLower(t) == target })
} }
return SliceContains(slice, target) return slices.Contains(slice, target)
} }
// SliceSortedEqual returns true if the two slices will be equal when they get sorted. // SliceSortedEqual returns true if the two slices will be equal when they get sorted.
@ -57,34 +41,7 @@ func SliceSortedEqual[T comparable](s1, s2 []T) bool {
return true return true
} }
// SliceEqual returns true if the two slices are equal.
func SliceEqual[T comparable](s1, s2 []T) bool {
if len(s1) != len(s2) {
return false
}
for i, v := range s1 {
if s2[i] != v {
return false
}
}
return true
}
// SliceRemoveAll removes all the target elements from the slice. // SliceRemoveAll removes all the target elements from the slice.
func SliceRemoveAll[T comparable](slice []T, target T) []T { func SliceRemoveAll[T comparable](slice []T, target T) []T {
return SliceRemoveAllFunc(slice, func(t T) bool { return t == target }) return slices.DeleteFunc(slice, func(t T) bool { return t == target })
}
// SliceRemoveAllFunc removes all elements which satisfy the targetFunc from the slice.
func SliceRemoveAllFunc[T comparable](slice []T, targetFunc func(T) bool) []T {
idx := 0
for _, v := range slice {
if targetFunc(v) {
continue
}
slice[idx] = v
idx++
}
return slice[:idx]
} }

View File

@ -9,20 +9,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestSliceContains(t *testing.T) {
assert.True(t, SliceContains([]int{2, 0, 2, 3}, 2))
assert.True(t, SliceContains([]int{2, 0, 2, 3}, 0))
assert.True(t, SliceContains([]int{2, 0, 2, 3}, 3))
assert.True(t, SliceContains([]string{"2", "0", "2", "3"}, "0"))
assert.True(t, SliceContains([]float64{2, 0, 2, 3}, 0))
assert.True(t, SliceContains([]bool{false, true, false}, true))
assert.False(t, SliceContains([]int{2, 0, 2, 3}, 4))
assert.False(t, SliceContains([]int{}, 4))
assert.False(t, SliceContains(nil, 4))
}
func TestSliceContainsString(t *testing.T) { func TestSliceContainsString(t *testing.T) {
assert.True(t, SliceContainsString([]string{"c", "b", "a", "b"}, "a")) assert.True(t, SliceContainsString([]string{"c", "b", "a", "b"}, "a"))
assert.True(t, SliceContainsString([]string{"c", "b", "a", "b"}, "b")) assert.True(t, SliceContainsString([]string{"c", "b", "a", "b"}, "b"))
@ -54,25 +40,6 @@ func TestSliceSortedEqual(t *testing.T) {
assert.False(t, SliceSortedEqual([]int{2, 0, 0, 3}, []int{2, 0, 2, 3})) assert.False(t, SliceSortedEqual([]int{2, 0, 0, 3}, []int{2, 0, 2, 3}))
} }
func TestSliceEqual(t *testing.T) {
assert.True(t, SliceEqual([]int{2, 0, 2, 3}, []int{2, 0, 2, 3}))
assert.True(t, SliceEqual([]int{}, []int{}))
assert.True(t, SliceEqual([]int(nil), nil))
assert.True(t, SliceEqual([]int(nil), []int{}))
assert.True(t, SliceEqual([]int{}, []int{}))
assert.True(t, SliceEqual([]string{"2", "0", "2", "3"}, []string{"2", "0", "2", "3"}))
assert.True(t, SliceEqual([]float64{2, 0, 2, 3}, []float64{2, 0, 2, 3}))
assert.True(t, SliceEqual([]bool{false, true, false}, []bool{false, true, false}))
assert.False(t, SliceEqual([]int{3, 0, 2, 2}, []int{2, 0, 2, 3}))
assert.False(t, SliceEqual([]int{2, 0, 2}, []int{2, 0, 2, 3}))
assert.False(t, SliceEqual([]int{}, []int{2, 0, 2, 3}))
assert.False(t, SliceEqual(nil, []int{2, 0, 2, 3}))
assert.False(t, SliceEqual([]int{2, 0, 2, 4}, []int{2, 0, 2, 3}))
assert.False(t, SliceEqual([]int{2, 0, 0, 3}, []int{2, 0, 2, 3}))
}
func TestSliceRemoveAll(t *testing.T) { func TestSliceRemoveAll(t *testing.T) {
assert.ElementsMatch(t, []int{2, 2, 3}, SliceRemoveAll([]int{2, 0, 2, 3}, 0)) assert.ElementsMatch(t, []int{2, 2, 3}, SliceRemoveAll([]int{2, 0, 2, 3}, 0))
assert.ElementsMatch(t, []int{0, 3}, SliceRemoveAll([]int{2, 0, 2, 3}, 2)) assert.ElementsMatch(t, []int{0, 3}, SliceRemoveAll([]int{2, 0, 2, 3}, 2))

View File

@ -4,6 +4,7 @@ explore=Erkunden
help=Hilfe help=Hilfe
logo=Logo logo=Logo
sign_in=Anmelden sign_in=Anmelden
sign_in_with_provider=Anmelden mit %s
sign_in_or=oder sign_in_or=oder
sign_out=Abmelden sign_out=Abmelden
sign_up=Registrieren sign_up=Registrieren
@ -79,6 +80,7 @@ milestones=Meilensteine
ok=OK ok=OK
cancel=Abbrechen cancel=Abbrechen
retry=Erneut versuchen
rerun=Neu starten rerun=Neu starten
rerun_all=Alle Jobs neu starten rerun_all=Alle Jobs neu starten
save=Speichern save=Speichern
@ -91,6 +93,7 @@ edit=Bearbeiten
enabled=Aktiviert enabled=Aktiviert
disabled=Deaktiviert disabled=Deaktiviert
locked=Gesperrt
copy=Kopieren copy=Kopieren
copy_url=URL kopieren copy_url=URL kopieren
@ -128,7 +131,9 @@ concept_user_organization=Organisation
show_timestamps=Zeitstempel anzeigen show_timestamps=Zeitstempel anzeigen
show_log_seconds=Sekunden anzeigen show_log_seconds=Sekunden anzeigen
show_full_screen=Vollbild anzeigen show_full_screen=Vollbild anzeigen
download_logs=Logs herunterladen
confirm_delete_selected=Alle ausgewählten Elemente löschen?
name=Name name=Name
value=Wert value=Wert
@ -167,6 +172,7 @@ string.desc=ZA
[error] [error]
occurred=Ein Fehler ist aufgetreten occurred=Ein Fehler ist aufgetreten
report_message=Wenn du glaubst, dass dies ein Fehler von Gitea ist, suche bitte auf <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a> nach diesem Fehler und erstelle gegebenenfalls einen neuen Bugreport.
missing_csrf=Fehlerhafte Anfrage: Kein CSRF Token verfügbar missing_csrf=Fehlerhafte Anfrage: Kein CSRF Token verfügbar
invalid_csrf=Fehlerhafte Anfrage: Ungültiger CSRF Token invalid_csrf=Fehlerhafte Anfrage: Ungültiger CSRF Token
not_found=Das Ziel konnte nicht gefunden werden. not_found=Das Ziel konnte nicht gefunden werden.
@ -220,6 +226,7 @@ repo_path_helper=Remote-Git-Repositories werden in diesem Verzeichnis gespeicher
lfs_path=Git-LFS-Wurzelpfad lfs_path=Git-LFS-Wurzelpfad
lfs_path_helper=In diesem Verzeichnis werden die Dateien von Git LFS abgespeichert. Leer lassen, um LFS zu deaktivieren. lfs_path_helper=In diesem Verzeichnis werden die Dateien von Git LFS abgespeichert. Leer lassen, um LFS zu deaktivieren.
run_user=Ausführen als run_user=Ausführen als
run_user_helper=Der Nutzer unter dem Gitea ausgeführt wird. Beachte, dass dieser Nutzer Zugriff auf das Repository-Wurzelverzeichnis haben muss.
domain=Server-Domain domain=Server-Domain
domain_helper=Domain oder Host-Adresse für den Server. domain_helper=Domain oder Host-Adresse für den Server.
ssh_port=SSH-Server-Port ssh_port=SSH-Server-Port
@ -291,6 +298,8 @@ invalid_password_algorithm=Ungültiger Passwort-Hash-Algorithmus
password_algorithm_helper=Lege einen Passwort-Hashing-Algorithmus fest. Algorithmen haben unterschiedliche Anforderungen und Stärken. Der argon2-Algorithmus ist ziemlich sicher, aber er verbraucht viel Speicher und kann für kleine Systeme ungeeignet sein. password_algorithm_helper=Lege einen Passwort-Hashing-Algorithmus fest. Algorithmen haben unterschiedliche Anforderungen und Stärken. Der argon2-Algorithmus ist ziemlich sicher, aber er verbraucht viel Speicher und kann für kleine Systeme ungeeignet sein.
enable_update_checker=Aktualisierungsprüfung aktivieren enable_update_checker=Aktualisierungsprüfung aktivieren
enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen. enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen.
env_config_keys=Umgebungskonfiguration
env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet:
[home] [home]
uname_holder=E-Mail-Adresse oder Benutzername uname_holder=E-Mail-Adresse oder Benutzername
@ -353,6 +362,7 @@ remember_me=Dieses Gerät speichern
forgot_password_title=Passwort vergessen forgot_password_title=Passwort vergessen
forgot_password=Passwort vergessen? forgot_password=Passwort vergessen?
sign_up_now=Noch kein Konto? Jetzt registrieren. sign_up_now=Noch kein Konto? Jetzt registrieren.
sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen!
confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die Registrierung abzuschließen. confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die Registrierung abzuschließen.
must_change_password=Aktualisiere dein Passwort must_change_password=Aktualisiere dein Passwort
allow_password_change=Verlange vom Benutzer das Passwort zu ändern (empfohlen) allow_password_change=Verlange vom Benutzer das Passwort zu ändern (empfohlen)
@ -587,6 +597,7 @@ user_bio=Biografie
disabled_public_activity=Dieser Benutzer hat die öffentliche Sichtbarkeit der Aktivität deaktiviert. disabled_public_activity=Dieser Benutzer hat die öffentliche Sichtbarkeit der Aktivität deaktiviert.
email_visibility.limited=Ihre E-Mail-Adresse ist für alle authentifizierten Benutzer sichtbar email_visibility.limited=Ihre E-Mail-Adresse ist für alle authentifizierten Benutzer sichtbar
email_visibility.private=Deine E-Mail-Adresse ist nur für Dich und Administratoren sichtbar email_visibility.private=Deine E-Mail-Adresse ist nur für Dich und Administratoren sichtbar
settings=Benutzereinstellungen
form.name_reserved=Der Benutzername "%s" ist reserviert. form.name_reserved=Der Benutzername "%s" ist reserviert.
form.name_pattern_not_allowed=Das Muster "%s" ist nicht in einem Benutzernamen erlaubt. form.name_pattern_not_allowed=Das Muster "%s" ist nicht in einem Benutzernamen erlaubt.
@ -662,6 +673,7 @@ update_user_avatar_success=Der Avatar des Benutzers wurde aktualisiert.
change_password=Passwort aktualisieren change_password=Passwort aktualisieren
old_password=Aktuelles Passwort old_password=Aktuelles Passwort
new_password=Neues Passwort new_password=Neues Passwort
retype_new_password=Neues Passwort bestätigen
password_incorrect=Das aktuelle Passwort ist falsch. password_incorrect=Das aktuelle Passwort ist falsch.
change_password_success=Dein Passwort wurde aktualisiert. Bitte verwende dieses beim nächsten Einloggen. change_password_success=Dein Passwort wurde aktualisiert. Bitte verwende dieses beim nächsten Einloggen.
password_change_disabled=Benutzer, die nicht von Gitea verwaltet werden, können ihr Passwort im Web-Interface nicht ändern. password_change_disabled=Benutzer, die nicht von Gitea verwaltet werden, können ihr Passwort im Web-Interface nicht ändern.
@ -1330,6 +1342,7 @@ issues.delete_branch_at=`löschte die Branch <b>%s</b> %s`
issues.filter_label=Label issues.filter_label=Label
issues.filter_label_exclude=„<code>Alt</code> + <code>Klick/Enter</code> verwenden, um Label auszuschließen” issues.filter_label_exclude=„<code>Alt</code> + <code>Klick/Enter</code> verwenden, um Label auszuschließen”
issues.filter_label_no_select=Alle Label issues.filter_label_no_select=Alle Label
issues.filter_label_select_no_label=Kein Label
issues.filter_milestone=Meilenstein issues.filter_milestone=Meilenstein
issues.filter_milestone_all=Alle Meilensteine issues.filter_milestone_all=Alle Meilensteine
issues.filter_milestone_none=Keine Meilensteine issues.filter_milestone_none=Keine Meilensteine
@ -1383,6 +1396,7 @@ issues.next=Nächste
issues.open_title=Offen issues.open_title=Offen
issues.closed_title=Geschlossen issues.closed_title=Geschlossen
issues.draft_title=Entwurf issues.draft_title=Entwurf
issues.num_comments_1=%d Kommentar
issues.num_comments=%d Kommentare issues.num_comments=%d Kommentare
issues.commented_at=`hat <a href="#%s">%s</a> kommentiert` issues.commented_at=`hat <a href="#%s">%s</a> kommentiert`
issues.delete_comment_confirm=Bist du sicher dass du diesen Kommentar löschen möchtest? issues.delete_comment_confirm=Bist du sicher dass du diesen Kommentar löschen möchtest?
@ -1391,6 +1405,7 @@ issues.context.quote_reply=Antwort zitieren
issues.context.reference_issue=In neuem Issue referenzieren issues.context.reference_issue=In neuem Issue referenzieren
issues.context.edit=Bearbeiten issues.context.edit=Bearbeiten
issues.context.delete=Löschen issues.context.delete=Löschen
issues.no_content=Keine Beschreibung angegeben.
issues.close=Issue schließen issues.close=Issue schließen
issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged
issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged
@ -1557,6 +1572,8 @@ issues.review.pending.tooltip=Dieser Kommentar ist derzeit nicht für andere Ben
issues.review.review=Review issues.review.review=Review
issues.review.reviewers=Reviewer issues.review.reviewers=Reviewer
issues.review.outdated=Veraltet issues.review.outdated=Veraltet
issues.review.option.show_outdated_comments=Veraltete Kommentare anzeigen
issues.review.option.hide_outdated_comments=Veraltete Kommentare ausblenden
issues.review.show_outdated=Veraltete anzeigen issues.review.show_outdated=Veraltete anzeigen
issues.review.hide_outdated=Veraltete ausblenden issues.review.hide_outdated=Veraltete ausblenden
issues.review.show_resolved=Gelöste anzeigen issues.review.show_resolved=Gelöste anzeigen
@ -1702,6 +1719,7 @@ pulls.delete.title=Diesen Pull-Request löschen?
pulls.delete.text=Willst du diesen Pull-Request wirklich löschen? (Dies wird den Inhalt unwiderruflich löschen. Überlege, ob du ihn nicht lieber schließen willst, um ihn zu archivieren) pulls.delete.text=Willst du diesen Pull-Request wirklich löschen? (Dies wird den Inhalt unwiderruflich löschen. Überlege, ob du ihn nicht lieber schließen willst, um ihn zu archivieren)
pull.deleted_branch=(gelöscht):%s
milestones.new=Neuer Meilenstein milestones.new=Neuer Meilenstein
milestones.closed=Geschlossen %s milestones.closed=Geschlossen %s

View File

@ -681,7 +681,7 @@ choose_new_avatar = Choose new avatar
update_avatar = Update Avatar update_avatar = Update Avatar
delete_current_avatar = Delete Current Avatar delete_current_avatar = Delete Current Avatar
uploaded_avatar_not_a_image = The uploaded file is not an image. uploaded_avatar_not_a_image = The uploaded file is not an image.
uploaded_avatar_is_too_big = The uploaded file has exceeded the maximum size. uploaded_avatar_is_too_big = The uploaded file size (%d KiB) exceeds the maximum size (%d KiB).
update_avatar_success = Your avatar has been updated. update_avatar_success = Your avatar has been updated.
update_user_avatar_success = The user's avatar has been updated. update_user_avatar_success = The user's avatar has been updated.
@ -1764,6 +1764,7 @@ pulls.rebase_conflict_summary = Error Message
pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy
pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again. pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again.
pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again. pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again.
pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch.
pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository. pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository.
pulls.push_rejected_summary = Full Rejection Message pulls.push_rejected_summary = Full Rejection Message
pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<br>Review the Git Hooks for this repository pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<br>Review the Git Hooks for this repository
@ -2730,6 +2731,7 @@ dashboard.reinit_missing_repos = Reinitialize all missing Git repositories for w
dashboard.sync_external_users = Synchronize external user data dashboard.sync_external_users = Synchronize external user data
dashboard.cleanup_hook_task_table = Cleanup hook_task table dashboard.cleanup_hook_task_table = Cleanup hook_task table
dashboard.cleanup_packages = Cleanup expired packages dashboard.cleanup_packages = Cleanup expired packages
dashboard.cleanup_actions = Cleanup actions expired logs and artifacts
dashboard.server_uptime = Server Uptime dashboard.server_uptime = Server Uptime
dashboard.current_goroutine = Current Goroutines dashboard.current_goroutine = Current Goroutines
dashboard.current_memory_usage = Current Memory Usage dashboard.current_memory_usage = Current Memory Usage
@ -3502,6 +3504,7 @@ runners.reset_registration_token_success = Runner registration token reset succe
runs.all_workflows = All Workflows runs.all_workflows = All Workflows
runs.commit = Commit runs.commit = Commit
runs.scheduled = Scheduled
runs.pushed_by = pushed by runs.pushed_by = pushed by
runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
runs.no_matching_runner_helper = No matching runner: %s runs.no_matching_runner_helper = No matching runner: %s

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ explore=エクスプローラー
help=ヘルプ help=ヘルプ
logo=ロゴ logo=ロゴ
sign_in=サインイン sign_in=サインイン
sign_in_with_provider=%s でサインイン
sign_in_or=または sign_in_or=または
sign_out=サインアウト sign_out=サインアウト
sign_up=登録 sign_up=登録
@ -91,6 +92,7 @@ edit=編集
enabled=有効 enabled=有効
disabled=無効 disabled=無効
locked=ロック済み
copy=コピー copy=コピー
copy_url=URLをコピー copy_url=URLをコピー
@ -128,7 +130,9 @@ concept_user_organization=組織
show_timestamps=タイムスタンプを表示 show_timestamps=タイムスタンプを表示
show_log_seconds=秒数を表示 show_log_seconds=秒数を表示
show_full_screen=フルスクリーン表示 show_full_screen=フルスクリーン表示
download_logs=ログをダウンロード
confirm_delete_selected=選択したすべてのアイテムを削除してよろしいですか?
name=名称 name=名称
value= value=
@ -221,6 +225,7 @@ repo_path_helper=リモートGitリポジトリはこのディレクトリに保
lfs_path=Git LFSルートパス lfs_path=Git LFSルートパス
lfs_path_helper=Git LFSで管理するファイルが、このディレクトリに保存されます。 空欄にするとGit LFSを無効にします。 lfs_path_helper=Git LFSで管理するファイルが、このディレクトリに保存されます。 空欄にするとGit LFSを無効にします。
run_user=実行ユーザー名 run_user=実行ユーザー名
run_user_helper=オペレーティングシステム上のユーザー名です。 Giteaをこのユーザーとして実行します。 このユーザーはリポジトリルートパスへのアクセス権を持っている必要があります。
domain=サーバードメイン domain=サーバードメイン
domain_helper=サーバーのドメインまたはホストアドレス。 domain_helper=サーバーのドメインまたはホストアドレス。
ssh_port=SSHサーバーのポート ssh_port=SSHサーバーのポート
@ -292,6 +297,8 @@ invalid_password_algorithm=無効なパスワードハッシュアルゴリズ
password_algorithm_helper=パスワードハッシュアルゴリズムを設定します。 アルゴリズムにより動作要件と強度が異なります。 argon2アルゴリズムはかなり安全ですが、多くのメモリを使用するため小さなシステムには適さない場合があります。 password_algorithm_helper=パスワードハッシュアルゴリズムを設定します。 アルゴリズムにより動作要件と強度が異なります。 argon2アルゴリズムはかなり安全ですが、多くのメモリを使用するため小さなシステムには適さない場合があります。
enable_update_checker=アップデートチェッカーを有効にする enable_update_checker=アップデートチェッカーを有効にする
enable_update_checker_helper=gitea.ioに接続して定期的に新しいバージョンのリリースを確認します。 enable_update_checker_helper=gitea.ioに接続して定期的に新しいバージョンのリリースを確認します。
env_config_keys=環境設定
env_config_keys_prompt=以下の環境変数も設定ファイルに適用されます:
[home] [home]
uname_holder=ユーザー名またはメールアドレス uname_holder=ユーザー名またはメールアドレス
@ -407,6 +414,7 @@ authorize_application_created_by=このアプリケーションは %s が作成
authorize_application_description=アクセスを許可すると、このアプリケーションは、プライベート リポジトリや組織を含むあなたのすべてのアカウント情報に対して、アクセスと書き込みができるようになります。 authorize_application_description=アクセスを許可すると、このアプリケーションは、プライベート リポジトリや組織を含むあなたのすべてのアカウント情報に対して、アクセスと書き込みができるようになります。
authorize_title=`"%s"にあなたのアカウントへのアクセスを許可しますか?` authorize_title=`"%s"にあなたのアカウントへのアクセスを許可しますか?`
authorization_failed=認可失敗 authorization_failed=認可失敗
authorization_failed_desc=無効なリクエストを検出したため認可が失敗しました。 認可しようとしたアプリの開発者に連絡してください。
sspi_auth_failed=SSPI認証に失敗しました sspi_auth_failed=SSPI認証に失敗しました
password_pwned=あなたが選択したパスワードは、過去の情報漏洩事件で流出した<a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">盗まれたパスワードのリスト</a>に含まれています。 別のパスワードでもう一度試してください。 また他の登録でもこのパスワードからの変更を検討してください。 password_pwned=あなたが選択したパスワードは、過去の情報漏洩事件で流出した<a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">盗まれたパスワードのリスト</a>に含まれています。 別のパスワードでもう一度試してください。 また他の登録でもこのパスワードからの変更を検討してください。
password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした
@ -594,6 +602,8 @@ user_bio=経歴
disabled_public_activity=このユーザーはアクティビティ表示を公開していません。 disabled_public_activity=このユーザーはアクティビティ表示を公開していません。
email_visibility.limited=あなたのメールアドレスはすべての認証済みユーザーに表示されています email_visibility.limited=あなたのメールアドレスはすべての認証済みユーザーに表示されています
email_visibility.private=あなたのメールアドレスは、あなたと管理者のみに表示されます email_visibility.private=あなたのメールアドレスは、あなたと管理者のみに表示されます
show_on_map=地図上にこの場所を表示
settings=ユーザー設定
form.name_reserved=ユーザー名 "%s" は予約されています。 form.name_reserved=ユーザー名 "%s" は予約されています。
form.name_pattern_not_allowed=`"%s" の形式はユーザー名に使用できません。` form.name_pattern_not_allowed=`"%s" の形式はユーザー名に使用できません。`
@ -620,6 +630,7 @@ webauthn=セキュリティキー
public_profile=公開プロフィール public_profile=公開プロフィール
biography_placeholder=自己紹介してください!(Markdownを使うことができます) biography_placeholder=自己紹介してください!(Markdownを使うことができます)
location_placeholder=おおよその場所を他の人と共有
profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。 profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。
password_username_disabled=非ローカルユーザーのユーザー名は変更できません。詳細はサイト管理者にお問い合わせください。 password_username_disabled=非ローカルユーザーのユーザー名は変更できません。詳細はサイト管理者にお問い合わせください。
full_name=フルネーム full_name=フルネーム
@ -632,6 +643,8 @@ update_language_not_found=言語 "%s" は利用できません。
update_language_success=言語が更新されました。 update_language_success=言語が更新されました。
update_profile_success=プロフィールを更新しました。 update_profile_success=プロフィールを更新しました。
change_username=ユーザー名を変更しました。 change_username=ユーザー名を変更しました。
change_username_prompt=注意: ユーザー名を変更するとアカウントのURLも変更されます。
change_username_redirect_prompt=古いユーザー名は、誰かが再使用するまではリダイレクトします。
continue=続行 continue=続行
cancel=キャンセル cancel=キャンセル
language=言語 language=言語
@ -656,6 +669,7 @@ comment_type_group_project=プロジェクト
comment_type_group_issue_ref=イシューの参照先 comment_type_group_issue_ref=イシューの参照先
saved_successfully=設定は正常に保存されました。 saved_successfully=設定は正常に保存されました。
privacy=プライバシー privacy=プライバシー
keep_activity_private=プロフィールページのアクティビティ表示を隠す
keep_activity_private_popup=アクティビティを、あなたと管理者にのみ表示します keep_activity_private_popup=アクティビティを、あなたと管理者にのみ表示します
lookup_avatar_by_mail=メールアドレスでアバターを見つける lookup_avatar_by_mail=メールアドレスでアバターを見つける
@ -689,6 +703,7 @@ requires_activation=アクティベーションが必要
primary_email=プライマリーにする primary_email=プライマリーにする
activate_email=アクティベーションを送信 activate_email=アクティベーションを送信
activations_pending=アクティベーション待ち activations_pending=アクティベーション待ち
can_not_add_email_activations_pending=保留中のアクティベーションがあります。新しいメールを追加する場合は、数分後にもう一度お試しください。
delete_email=削除 delete_email=削除
email_deletion=メールアドレスの削除 email_deletion=メールアドレスの削除
email_deletion_desc=メールアドレスと関連情報をアカウントから削除します。 このメールアドレスを使ったGitのコミットはそのまま残ります。 続行しますか? email_deletion_desc=メールアドレスと関連情報をアカウントから削除します。 このメールアドレスを使ったGitのコミットはそのまま残ります。 続行しますか?
@ -807,8 +822,10 @@ repo_and_org_access=リポジトリと組織へのアクセス
permissions_public_only=公開のみ permissions_public_only=公開のみ
permissions_access_all=すべて (公開、プライベート、限定) permissions_access_all=すべて (公開、プライベート、限定)
select_permissions=許可の選択 select_permissions=許可の選択
permission_no_access=アクセスなし permission_no_access=アクセス不可
permission_read=既読 permission_read=読み取り
permission_write=読み取りと書き込み
access_token_desc=選択したトークン権限に応じて、関連する<a %s>API</a>ルートのみに許可が制限されます。 詳細は<a %s>ドキュメント</a>を参照してください。
at_least_one_permission=トークンを作成するには、少なくともひとつの許可を選択する必要があります at_least_one_permission=トークンを作成するには、少なくともひとつの許可を選択する必要があります
permissions_list=許可: permissions_list=許可:
@ -834,6 +851,7 @@ oauth2_client_secret_hint=このページから移動したりページを更新
oauth2_application_edit=編集 oauth2_application_edit=編集
oauth2_application_create_description=OAuth2アプリケーションで、サードパーティアプリケーションがこのインスタンス上のユーザーアカウントにアクセスできるようになります。 oauth2_application_create_description=OAuth2アプリケーションで、サードパーティアプリケーションがこのインスタンス上のユーザーアカウントにアクセスできるようになります。
oauth2_application_remove_description=OAuth2アプリケーションを削除すると、このインスタンス上の許可されたユーザーアカウントへのアクセスができなくなります。 続行しますか? oauth2_application_remove_description=OAuth2アプリケーションを削除すると、このインスタンス上の許可されたユーザーアカウントへのアクセスができなくなります。 続行しますか?
oauth2_application_locked=設定で有効にされた場合、Giteaは起動時にいくつかのOAuth2アプリケーションを事前登録します。 想定されていない動作を防ぐため、これらは編集も削除もできません。 詳細についてはOAuth2のドキュメントを参照してください。
authorized_oauth2_applications=許可済みOAuth2アプリケーション authorized_oauth2_applications=許可済みOAuth2アプリケーション
authorized_oauth2_applications_description=これらのサードパーティ アプリケーションに、あなたのGiteaアカウントへのアクセスを許可しています。 不要になったアプリケーションはアクセス権を取り消すようにしてください。 authorized_oauth2_applications_description=これらのサードパーティ アプリケーションに、あなたのGiteaアカウントへのアクセスを許可しています。 不要になったアプリケーションはアクセス権を取り消すようにしてください。
@ -922,6 +940,7 @@ fork_from=フォーク元
already_forked=%s はフォーク済み already_forked=%s はフォーク済み
fork_to_different_account=別のアカウントにフォークする fork_to_different_account=別のアカウントにフォークする
fork_visibility_helper=フォークしたリポジトリの公開/非公開は変更できません。 fork_visibility_helper=フォークしたリポジトリの公開/非公開は変更できません。
fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。
use_template=このテンプレートを使用 use_template=このテンプレートを使用
clone_in_vsc=VSCodeでクローン clone_in_vsc=VSCodeでクローン
download_zip=ZIPファイルをダウンロード download_zip=ZIPファイルをダウンロード
@ -2522,6 +2541,7 @@ settings.visibility.private_shortname=プライベート
settings.update_settings=設定の更新 settings.update_settings=設定の更新
settings.update_setting_success=組織の設定を更新しました。 settings.update_setting_success=組織の設定を更新しました。
settings.change_orgname_prompt=注意: 組織名を変更すると組織のURLも変更され、古い名前は解放されます。
settings.change_orgname_redirect_prompt=古い名前は、再使用されていない限りリダイレクトします。 settings.change_orgname_redirect_prompt=古い名前は、再使用されていない限りリダイレクトします。
settings.update_avatar_success=組織のアバターを更新しました。 settings.update_avatar_success=組織のアバターを更新しました。
settings.delete=組織を削除 settings.delete=組織を削除
@ -2615,6 +2635,7 @@ monitor=モニタリング
first_page=最初 first_page=最初
last_page=最後 last_page=最後
total=合計: %d total=合計: %d
settings=管理設定
dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">ブログ</a> を確認してください。 dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">ブログ</a> を確認してください。
dashboard.statistic=サマリー dashboard.statistic=サマリー
@ -3378,6 +3399,7 @@ status.waiting=待機中
status.running=実行中 status.running=実行中
status.success=成功 status.success=成功
status.failure=失敗 status.failure=失敗
status.cancelled=キャンセル
status.skipped=スキップ status.skipped=スキップ
status.blocked=ブロックされた status.blocked=ブロックされた
@ -3432,5 +3454,10 @@ type-2.display_name=リポジトリ プロジェクト
type-3.display_name=組織プロジェクト type-3.display_name=組織プロジェクト
[git.filemode] [git.filemode]
symbolic_link=シンボリック リンク changed_filemode=%[1]s → %[2]s
directory=ディレクトリ
normal_file=ノーマルファイル
executable_file=実行可能ファイル
symbolic_link=シンボリックリンク
submodule=サブモジュール

View File

@ -20,7 +20,7 @@ active_stopwatch=Cronómetro em andamento
create_new=Criar… create_new=Criar…
user_profile_and_more=Perfil e configurações… user_profile_and_more=Perfil e configurações…
signed_in_as=Sessão iniciada como signed_in_as=Sessão iniciada como
enable_javascript=Este website requer JavaScript. enable_javascript=Este sítio Web requer JavaScript.
toc=Índice toc=Índice
licenses=Licenças licenses=Licenças
return_to_gitea=Retornar ao Gitea return_to_gitea=Retornar ao Gitea

12
package-lock.json generated
View File

@ -27,7 +27,6 @@
"escape-goat": "4.0.0", "escape-goat": "4.0.0",
"fast-glob": "3.3.1", "fast-glob": "3.3.1",
"jquery": "3.7.1", "jquery": "3.7.1",
"jquery.are-you-sure": "1.9.0",
"katex": "0.16.8", "katex": "0.16.8",
"license-checker-webpack-plugin": "0.2.1", "license-checker-webpack-plugin": "0.2.1",
"lightningcss-loader": "2.1.0", "lightningcss-loader": "2.1.0",
@ -6466,17 +6465,6 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
}, },
"node_modules/jquery.are-you-sure": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/jquery.are-you-sure/-/jquery.are-you-sure-1.9.0.tgz",
"integrity": "sha512-2r0uFx8CyAopjeHGOdvvwpFP921TnW1+v1uJXcAWQYHYGB1tryTDhQY+5u6HsVeMwbWiRTKVZFWnLaFpDvIqZQ==",
"dependencies": {
"jquery": ">=1.4.2"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/js-levenshtein-esm": { "node_modules/js-levenshtein-esm": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz", "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz",

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